Repository: kyleneideck/BackgroundMusic Branch: master Commit: 7ea90878fa80 Files: 289 Total size: 2.2 MB Directory structure: gitextract_t8bnn9v3/ ├── .editorconfig ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── other.md │ └── workflows/ │ └── build-test-release.yml ├── .gitignore ├── BGM.xcworkspace/ │ └── contents.xcworkspacedata ├── BGMApp/ │ ├── BGMApp/ │ │ ├── BGMApp-Debug.entitlements │ │ ├── BGMApp.entitlements │ │ ├── BGMAppDelegate.h │ │ ├── BGMAppDelegate.mm │ │ ├── BGMAppVolumes.h │ │ ├── BGMAppVolumes.m │ │ ├── BGMAppVolumesController.h │ │ ├── BGMAppVolumesController.mm │ │ ├── BGMAppWatcher.h │ │ ├── BGMAppWatcher.m │ │ ├── BGMAudioDevice.cpp │ │ ├── BGMAudioDevice.h │ │ ├── BGMAudioDeviceManager.h │ │ ├── BGMAudioDeviceManager.mm │ │ ├── BGMAutoPauseMenuItem.h │ │ ├── BGMAutoPauseMenuItem.m │ │ ├── BGMAutoPauseMusic.h │ │ ├── BGMAutoPauseMusic.mm │ │ ├── BGMBackgroundMusicDevice.cpp │ │ ├── BGMBackgroundMusicDevice.h │ │ ├── BGMDebugLoggingMenuItem.h │ │ ├── BGMDebugLoggingMenuItem.m │ │ ├── BGMDeviceControlSync.cpp │ │ ├── BGMDeviceControlSync.h │ │ ├── BGMDeviceControlsList.cpp │ │ ├── BGMDeviceControlsList.h │ │ ├── BGMOutputDeviceMenuSection.h │ │ ├── BGMOutputDeviceMenuSection.mm │ │ ├── BGMOutputVolumeMenuItem.h │ │ ├── BGMOutputVolumeMenuItem.mm │ │ ├── BGMPlayThrough.cpp │ │ ├── BGMPlayThrough.h │ │ ├── BGMPlayThroughRTLogger.cpp │ │ ├── BGMPlayThroughRTLogger.h │ │ ├── BGMPreferredOutputDevices.h │ │ ├── BGMPreferredOutputDevices.mm │ │ ├── BGMStatusBarItem.h │ │ ├── BGMStatusBarItem.mm │ │ ├── BGMSystemSoundsVolume.h │ │ ├── BGMSystemSoundsVolume.mm │ │ ├── BGMTermination.h │ │ ├── BGMTermination.mm │ │ ├── BGMUserDefaults.h │ │ ├── BGMUserDefaults.m │ │ ├── BGMVolumeChangeListener.cpp │ │ ├── BGMVolumeChangeListener.h │ │ ├── BGMXPCListener.h │ │ ├── BGMXPCListener.mm │ │ ├── Base.lproj/ │ │ │ └── MainMenu.xib │ │ ├── Images.xcassets/ │ │ │ ├── AirPlayIcon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── FermataIcon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Volume0.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Volume1.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── Volume2.imageset/ │ │ │ │ └── Contents.json │ │ │ └── Volume3.imageset/ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── LICENSE │ │ ├── Music Players/ │ │ │ ├── BGMDecibel.h │ │ │ ├── BGMDecibel.m │ │ │ ├── BGMGooglePlayMusicDesktopPlayer.h │ │ │ ├── BGMGooglePlayMusicDesktopPlayer.m │ │ │ ├── BGMGooglePlayMusicDesktopPlayerConnection.h │ │ │ ├── BGMGooglePlayMusicDesktopPlayerConnection.m │ │ │ ├── BGMHermes.h │ │ │ ├── BGMHermes.m │ │ │ ├── BGMMusic.h │ │ │ ├── BGMMusic.m │ │ │ ├── BGMMusicPlayer.h │ │ │ ├── BGMMusicPlayer.m │ │ │ ├── BGMMusicPlayers.h │ │ │ ├── BGMMusicPlayers.mm │ │ │ ├── BGMScriptingBridge.h │ │ │ ├── BGMScriptingBridge.m │ │ │ ├── BGMSpotify.h │ │ │ ├── BGMSpotify.m │ │ │ ├── BGMSwinsian.h │ │ │ ├── BGMSwinsian.m │ │ │ ├── BGMVLC.h │ │ │ ├── BGMVLC.m │ │ │ ├── BGMVOX.h │ │ │ ├── BGMVOX.m │ │ │ ├── BGMiTunes.h │ │ │ ├── BGMiTunes.m │ │ │ ├── Decibel.h │ │ │ ├── GooglePlayMusicDesktopPlayer.js │ │ │ ├── Hermes.h │ │ │ ├── Music.h │ │ │ ├── Spotify.h │ │ │ ├── Swinsian.h │ │ │ ├── VLC.h │ │ │ ├── VOX.h │ │ │ └── iTunes.h │ │ ├── Preferences/ │ │ │ ├── BGMAboutPanel.h │ │ │ ├── BGMAboutPanel.m │ │ │ ├── BGMAutoPauseMusicPrefs.h │ │ │ ├── BGMAutoPauseMusicPrefs.mm │ │ │ ├── BGMPreferencesMenu.h │ │ │ └── BGMPreferencesMenu.mm │ │ ├── Scripting/ │ │ │ ├── BGMASApplication.h │ │ │ ├── BGMASApplication.m │ │ │ ├── BGMASOutputDevice.h │ │ │ ├── BGMASOutputDevice.mm │ │ │ ├── BGMApp.sdef │ │ │ ├── BGMAppDelegate+AppleScript.h │ │ │ └── BGMAppDelegate+AppleScript.mm │ │ ├── SystemPreferences.h │ │ ├── _uninstall-non-interactive.sh │ │ └── main.m │ ├── BGMApp.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── BGMXPCHelper.xcscheme │ │ └── Background Music.xcscheme │ ├── BGMAppTests/ │ │ ├── UITests/ │ │ │ ├── BGMApp.h │ │ │ ├── BGMAppUITests-Info.plist │ │ │ ├── BGMAppUITests.mm │ │ │ └── skip-ui-tests.py │ │ └── UnitTests/ │ │ ├── BGMAppUnitTests-Info.plist │ │ ├── BGMMusicPlayersUnitTests.mm │ │ ├── BGMPlayThroughRTLoggerTests.mm │ │ ├── BGMPlayThroughTests.mm │ │ └── Mocks/ │ │ ├── MockAudioDevice.cpp │ │ ├── MockAudioDevice.h │ │ ├── MockAudioObject.cpp │ │ ├── MockAudioObject.h │ │ ├── MockAudioObjects.cpp │ │ ├── MockAudioObjects.h │ │ ├── Mock_CAHALAudioDevice.cpp │ │ ├── Mock_CAHALAudioObject.cpp │ │ └── Mock_CAHALAudioSystemObject.cpp │ ├── BGMThreadSafetyAnalysis.h │ ├── BGMXPCHelper/ │ │ ├── BGMXPCHelperService.h │ │ ├── BGMXPCHelperService.mm │ │ ├── BGMXPCListenerDelegate.h │ │ ├── BGMXPCListenerDelegate.m │ │ ├── Info.plist │ │ ├── com.bearisdriving.BGM.XPCHelper.plist.template │ │ ├── main.m │ │ ├── post_install.sh │ │ └── safe_install_dir.sh │ ├── BGMXPCHelperTests/ │ │ ├── BGMXPCHelperTests-Info.plist │ │ └── BGMXPCHelperTests.m │ ├── OptimizationProfiles/ │ │ └── BGMApp.profdata │ └── PublicUtility/ │ ├── BGMDebugLogging.c │ ├── BGMDebugLogging.h │ ├── CAAtomic.h │ ├── CAAutoDisposer.h │ ├── CABitOperations.h │ ├── CACFArray.cpp │ ├── CACFArray.h │ ├── CACFDictionary.cpp │ ├── CACFDictionary.h │ ├── CACFNumber.cpp │ ├── CACFNumber.h │ ├── CACFString.cpp │ ├── CACFString.h │ ├── CADebugMacros.cpp │ ├── CADebugMacros.h │ ├── CADebugPrintf.cpp │ ├── CADebugPrintf.h │ ├── CADebugger.cpp │ ├── CADebugger.h │ ├── CAException.h │ ├── CAHALAudioDevice.cpp │ ├── CAHALAudioDevice.h │ ├── CAHALAudioObject.cpp │ ├── CAHALAudioObject.h │ ├── CAHALAudioStream.cpp │ ├── CAHALAudioStream.h │ ├── CAHALAudioSystemObject.cpp │ ├── CAHALAudioSystemObject.h │ ├── CAHostTimeBase.cpp │ ├── CAHostTimeBase.h │ ├── CAMutex.cpp │ ├── CAMutex.h │ ├── CAPThread.cpp │ ├── CAPThread.h │ ├── CAPropertyAddress.h │ ├── CARingBuffer.cpp │ └── CARingBuffer.h ├── BGMDriver/ │ ├── BGMDriver/ │ │ ├── BGM_AbstractDevice.cpp │ │ ├── BGM_AbstractDevice.h │ │ ├── BGM_AudibleState.cpp │ │ ├── BGM_AudibleState.h │ │ ├── BGM_Control.cpp │ │ ├── BGM_Control.h │ │ ├── BGM_Device.cpp │ │ ├── BGM_Device.h │ │ ├── BGM_MuteControl.cpp │ │ ├── BGM_MuteControl.h │ │ ├── BGM_NullDevice.cpp │ │ ├── BGM_NullDevice.h │ │ ├── BGM_Object.cpp │ │ ├── BGM_Object.h │ │ ├── BGM_PlugIn.cpp │ │ ├── BGM_PlugIn.h │ │ ├── BGM_PlugInInterface.cpp │ │ ├── BGM_Stream.cpp │ │ ├── BGM_Stream.h │ │ ├── BGM_TaskQueue.cpp │ │ ├── BGM_TaskQueue.h │ │ ├── BGM_VolumeControl.cpp │ │ ├── BGM_VolumeControl.h │ │ ├── BGM_WrappedAudioEngine.cpp │ │ ├── BGM_WrappedAudioEngine.h │ │ ├── BGM_XPCHelper.h │ │ ├── BGM_XPCHelper.m │ │ ├── DeviceClients/ │ │ │ ├── BGM_Client.cpp │ │ │ ├── BGM_Client.h │ │ │ ├── BGM_ClientMap.cpp │ │ │ ├── BGM_ClientMap.h │ │ │ ├── BGM_ClientTasks.h │ │ │ ├── BGM_Clients.cpp │ │ │ └── BGM_Clients.h │ │ ├── DeviceIcon.icns │ │ ├── Info.plist │ │ └── quick_install.sh │ ├── BGMDriver.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── Background Music Device.xcscheme │ │ └── PublicUtility.xcscheme │ ├── BGMDriverTests/ │ │ ├── BGM_ClientMapTests.mm │ │ ├── BGM_ClientsTests.mm │ │ ├── BGM_DeviceTests.mm │ │ └── Info.plist │ └── PublicUtility/ │ ├── CAAtomic.h │ ├── CAAtomicStack.h │ ├── CAAutoDisposer.h │ ├── CABitOperations.h │ ├── CACFArray.cpp │ ├── CACFArray.h │ ├── CACFDictionary.cpp │ ├── CACFDictionary.h │ ├── CACFNumber.cpp │ ├── CACFNumber.h │ ├── CACFString.cpp │ ├── CACFString.h │ ├── CADebugMacros.cpp │ ├── CADebugMacros.h │ ├── CADebugPrintf.cpp │ ├── CADebugPrintf.h │ ├── CADebugger.cpp │ ├── CADebugger.h │ ├── CADispatchQueue.cpp │ ├── CADispatchQueue.h │ ├── CAException.h │ ├── CAHostTimeBase.cpp │ ├── CAHostTimeBase.h │ ├── CAMutex.cpp │ ├── CAMutex.h │ ├── CAPThread.cpp │ ├── CAPThread.h │ ├── CAPropertyAddress.h │ ├── CARingBuffer.cpp │ ├── CARingBuffer.h │ ├── CAVolumeCurve.cpp │ └── CAVolumeCurve.h ├── CONTRIBUTING.md ├── DEVELOPING.md ├── Images/ │ ├── FermataIcon.tex │ ├── VolumeIcons.tex │ ├── generate_icon_pngs.sh │ └── iconizer.sh ├── LICENSE ├── LICENSE-Apple-Sample-Code ├── MANUAL-INSTALL.md ├── MANUAL-UNINSTALL.md ├── README.md ├── SharedSource/ │ ├── BGMXPCProtocols.h │ ├── BGM_TestUtils.h │ ├── BGM_Types.h │ ├── BGM_Utils.cpp │ ├── BGM_Utils.h │ └── Scripts/ │ └── set-version.sh ├── TODO.md ├── build_and_install.sh ├── package.sh ├── pkg/ │ ├── Distribution.xml.template │ ├── ListInputDevices.swift │ ├── README.md │ ├── pkgbuild.plist │ ├── postinstall │ └── preinstall └── uninstall.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # 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 ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- ## Example bug report template > Don't worry if you have trouble getting some of this info. Just leave it out. **Description of the bug** > Please don't just say it's "not working". **Steps to reproduce** > Steps to reproduce the bug. This usually doesn't need to be super detailed. 1. Go to '...' 2. Click on '...' 3. See error message '...' **Versions** > Please complete the following information. - Background Music: [e.g. "0.4.3" or "0.4.0-SNAPSHOT-c0ab98b". `Preferences > About Background Music`] - macOS: [e.g. "11.3 Beta (20E5172i)" or "Big Sur". ` > About This Mac`] **Hardware** > Delete this part if you think it's probably not necessary. - Computer: [e.g. "MacBook Pro (13-inch, 2016, Four Thunderbolt 3 Ports)". ` > About This Mac`] - Audio Device: [e.g. "Built-in Output. Manufacturer: Apple Inc. Output Channels: 2 [...]". `System Information app > Hardware > Audio`] **Debug logs** > If you think the developers might not be able to reproduce the bug on their computers, e.g. because an important feature is completely broken and they would have noticed, it can help to include [debug logs](https://github.com/kyleneideck/BackgroundMusic/wiki/Getting-Debug-Logs). This takes a little effort, so feel free to leave it out at first. [Debug logs attached here](https://github.com/example/background-music-debug-logs.txt) **Other info** > Anything else you want to add? --- > Tips > (Delete this section before posting.) > - https://github.com/kyleneideck/BackgroundMusic#troubleshooting > - Try the latest SNAPSHOT version from https://github.com/kyleneideck/BackgroundMusic/releases (if it's newer than the latest non-SNAPSHOT release). > - If your bug is one of these common issues, consider leaving a comment or a +1 (👍) on an existing issue: > - Background Music currently only supports audio devices with two channels. Bluetooth devices often only have one. > - Volumes having no effect for certain apps: Microsoft Teams ([workaround](https://github.com/kyleneideck/BackgroundMusic/issues/268#issuecomment-604977210)), Zoom ([workaround](https://github.com/kyleneideck/BackgroundMusic/issues/396#issuecomment-741992157)), Discord ([workaround](https://github.com/kyleneideck/BackgroundMusic/issues/210#issuecomment-507048957), [see also](https://github.com/kyleneideck/BackgroundMusic/issues/267#issuecomment-617327850)), Chrome (sometimes) ================================================ FILE: .github/ISSUE_TEMPLATE/other.md ================================================ --- name: Other about: Feature request, question, support request or anything else title: '' labels: '' assignees: '' --- > There's no template for this issue type. I just wanted to make it clear that it's OK to submit other types of issues. ================================================ FILE: .github/workflows/build-test-release.yml ================================================ # TODO: Split this into multiple .yml files? Multiple jobs? name: Build, Test and Release on: push: branches: - '*' tags: - '*' pull_request: branches: - '*' jobs: # Build and test in the same job because the UI tests expect BGMDriver to be installed. build-and-test: runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: matrix: # TODO: Add older macOS versions. os: - macos-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Work in a case-sensitive disk image. # This lets us catch failures that only happen on case-sensitive filesystems. run: | 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 - name: Install coreutils for actions/runner/issues/884 workaround. # See https://github.com/actions/runner/issues/884#issuecomment-1018851327 run: brew install coreutils - name: Build and install Background Music. run: | # `sudo` and `tput` expect this to be set. export TERM=xterm-256color genv --default-signal=PIPE yes | sudo ./build_and_install.sh - name: Print the log file. if: always() run: cat build_and_install.log - name: Log some checksums. run: 'find */build/Release/*/ -type f -exec md5 {} \;' - name: Log the installed audio devices and their IDs. run: | system_profiler SPAudioDataType say -a '?' - name: Check the BGM dirs and files were installed. run: | # These commands fail if the dir/file isn't found. ls -la "/Applications/Background Music.app" ls -la "/Library/Audio/Plug-Ins/HAL/Background Music Device.driver" ls -la "/usr/local/libexec/BGMXPCHelper.xpc" \ || ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc" ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist" - name: Close BGMApp (which the install script opened). run: >- osascript -e 'tell application "Background Music" to quit' || killall "Background Music" - name: Skip the UI tests. (They don't work on GitHub Actions yet.) run: BGMApp/BGMAppTests/UITests/skip-ui-tests.py - name: Run the tests. run: | echo '::group::BGMDriver Tests' xcodebuild \ -quiet \ -workspace BGM.xcworkspace \ -scheme 'Background Music Device' \ test echo '::endgroup::' echo '::group::BGMXPCHelper Tests' xcodebuild \ -quiet \ -workspace BGM.xcworkspace \ -scheme 'BGMXPCHelper' \ test echo '::endgroup::' # Grant BGMApp authorization to use input devices. # This is necessary for the UI tests because accepting the "Background Music would like to # use the microphone" dialog programmatically isn't reliable. # TODO: Commented out because we would need to generate the csreq (codesign signature) # value to match the BGMApp bundle the tests will run against. # dbPath="$HOME/Library/Application Support/com.apple.TCC/TCC.db" # values="'kTCCServiceMicrophone','com.bearisdriving.BGM.App',0,2,2,1,X'FADE0C000000004800000001000000070000000800000014545ABE68FAF437700B14984BB24117EDDA1BBF2C0000000800000014386FB63B9CD6BA6E83CEDEAF4EDEE177C1FAEA92',NULL,NULL,'UNUSED',NULL,0,1652845317" # sqlQuery="INSERT OR IGNORE INTO access VALUES($values);" # sqlite3 "$dbPath" "$sqlQuery" || (echo "Failed to modify $dbPath"; exit 1) # # Log the added TCC.db entry. # sqlite3 "$dbPath" "select * from access where client like '%BGM%';" echo '::group::BGMApp Tests' # TODO: Commented out in case it uses too much CPU. # log stream --info \ # --predicate 'process == "coreaudiod" or # process == "Background Music" or # process == "BGMXPCHelper" or # composedMessage contains[cd] "Background Music" or # composedMessage contains "BGM"' > app.log & xcodebuild \ -quiet \ -workspace BGM.xcworkspace \ -scheme 'Background Music' \ test echo '::endgroup::' - name: Upload the test results. if: always() uses: actions/upload-artifact@v4 with: name: bgm-test-results path: | /Users/runner/Library/Developer/Xcode/DerivedData/*/Logs/Test/*.xcresult app.log /Users/runner/Library/Logs/CrashReporter/* /Users/runner/Library/Logs/DiagnosticReports/* - name: Uninstall Background Music. run: | # `tput` expects this to be set. export TERM=xterm-256color genv --default-signal=PIPE yes | sudo ./uninstall.sh - name: Check the BGM dirs and files were removed. run: | if ls -la "/Applications/Background Music.app"; then exit 1; fi if ls -la "/Library/Audio/Plug-Ins/HAL/Background Music Device.driver"; then exit 1; fi if ls -la "/usr/local/libexec/BGMXPCHelper.xpc"; then exit 1; fi if ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc"; then exit 1 fi if ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist"; then exit 1; fi release: runs-on: macos-latest timeout-minutes: 15 steps: - name: Checkout uses: actions/checkout@v4 - name: Build the .pkg installer. run: | # `sudo` and `tput` expect this to be set. export TERM=xterm-256color # If this build is for a tag with "DEBUG" in its name, build a debug package. (More # detailed logging, no optimization, etc.) if [[ "$GITHUB_REF" =~ .*DEBUG.* ]]; then sudo ./package.sh -d else sudo ./package.sh fi - name: Install the .pkg. # Delete archives/ first because it contains a copy of Background Music.app. # Background Music.app is "relocatable", which means that if the user moves it and then # installs a new version, macOS will put the new version in the same place. This makes sure # the installer puts Background Music.app in /Applications so the build won't fail when we # check that later. # # package.sh puts the archives in a zipfile next to the .pkg, so we can still upload them # after deleting the directory here. # # TODO: On TravisCI, this was failing for debug builds. We couldn't figure out why, so we # might have to ignore that with # || [[ "$GITHUB_REF" =~ .*DEBUG.* ]] run: | sudo rm -rf archives sudo installer \ -pkg Background-Music-*/BackgroundMusic-*.pkg \ -target / \ -verbose \ -dumplog - name: Print the installer logs. if: always() # This trims the start of the log to save space. run: grep -E -A 9999 -B 20 'Background.?Music' /var/log/install.log - name: Check the BGM dirs and files were installed. if: always() run: | ls -la "/Applications/Background Music.app" ls -la "/Library/Audio/Plug-Ins/HAL/Background Music Device.driver" ls -la "/usr/local/libexec/BGMXPCHelper.xpc" \ || ls -la "/Library/Application Support/Background Music/BGMXPCHelper.xpc" ls -la "/Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist" - name: Upload the .pkg installer and archives. if: always() uses: actions/upload-artifact@v4 with: name: pkg-installer path: Background-Music-* - name: Upload the log file from the package.sh build. if: always() uses: actions/upload-artifact@v4 with: name: build-and-install-log-for-pkg path: build_and_install.log # TODO: Create a GitHub release. This is the Travis YAML that was handling it: # deploy: # provider: releases # api_key: # secure: j5Gd[...] # file_glob: true # file: Background-Music-*/* # skip_cleanup: true # name: $TRAVIS_TAG # prerelease: true # draft: true # on: # repo: kyleneideck/BackgroundMusic # tags: true # # TODO: Use "condition" to build master and tags? # condition: $DEPLOY = true ================================================ FILE: .gitignore ================================================ .DS_Store .*.swp /BGMDriver/BGMDriver/quick_install.conf /build_and_install.log .idea/ tags cmake-build-debug/ /Background-Music-*/ BGM.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist Images/*.aux Images/*.log /archives/ # Everything below is from https://github.com/github/gitignore/blob/master/Objective-C.gitignore ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata ## Other *.xccheckout *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa ================================================ FILE: BGM.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: BGMApp/BGMApp/BGMApp-Debug.entitlements ================================================ com.apple.security.automation.apple-events com.apple.security.device.audio-input com.apple.security.cs.disable-library-validation ================================================ FILE: BGMApp/BGMApp/BGMApp.entitlements ================================================ com.apple.security.automation.apple-events com.apple.security.device.audio-input ================================================ FILE: BGMApp/BGMApp/BGMAppDelegate.h ================================================ // 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 . // // BGMAppDelegate.h // BGMApp // // Copyright © 2016, 2017, 2020 Kyle Neideck // Copyright © 2021 Marcus Wu // // Sets up and tears down the app. // // System Includes #import @class BGMAudioDeviceManager; @class BGMAppVolumesController; // Tags for UI elements in MainMenu.xib static NSInteger const kVolumesHeadingMenuItemTag = 3; static NSInteger const kSeparatorBelowVolumesMenuItemTag = 4; @interface BGMAppDelegate : NSObject @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 (weak) IBOutlet NSMenuItem* debugLoggingMenuItemUnwrapped; @property (readonly) BGMAudioDeviceManager* audioDevices; @property BGMAppVolumesController* appVolumes; @end ================================================ FILE: BGMApp/BGMApp/BGMAppDelegate.mm ================================================ // 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 . // // BGMAppDelegate.mm // BGMApp // // Copyright © 2016-2022 Kyle Neideck // Copyright © 2021 Marcus Wu // // Self Include #import "BGMAppDelegate.h" // Local Includes #import "BGM_Utils.h" #import "BGMAppVolumes.h" #import "BGMAppVolumesController.h" #import "BGMAutoPauseMusic.h" #import "BGMAutoPauseMenuItem.h" #import "BGMDebugLoggingMenuItem.h" #import "BGMMusicPlayers.h" #import "BGMOutputDeviceMenuSection.h" #import "BGMOutputVolumeMenuItem.h" #import "BGMPreferencesMenu.h" #import "BGMPreferredOutputDevices.h" #import "BGMStatusBarItem.h" #import "BGMSystemSoundsVolume.h" #import "BGMTermination.h" #import "BGMUserDefaults.h" #import "BGMXPCListener.h" #import "SystemPreferences.h" // System Includes #import #pragma clang assume_nonnull begin static NSString* const kOptNoPersistentData = @"--no-persistent-data"; static NSString* const kOptShowDockIcon = @"--show-dock-icon"; @implementation BGMAppDelegate { // The button in the system status bar that shows the main menu. BGMStatusBarItem* statusBarItem; // Only show the 'BGMXPCHelper is missing' error dialog once. BOOL haveShownXPCHelperErrorMessage; // Persistently stores user settings and data. BGMUserDefaults* userDefaults; BGMAutoPauseMusic* autoPauseMusic; BGMAutoPauseMenuItem* autoPauseMenuItem; BGMMusicPlayers* musicPlayers; BGMSystemSoundsVolume* systemSoundsVolume; BGMOutputDeviceMenuSection* outputDeviceMenuSection; BGMPreferencesMenu* prefsMenu; BGMDebugLoggingMenuItem* debugLoggingMenuItem; BGMXPCListener* xpcListener; BGMPreferredOutputDevices* preferredOutputDevices; } @synthesize audioDevices = audioDevices; @synthesize appVolumes = appVolumes; - (void) awakeFromNib { [super awakeFromNib]; // Show BGMApp in the dock, if the command-line option for that was passed. This is used by the // UI tests. if ([NSProcessInfo.processInfo.arguments indexOfObject:kOptShowDockIcon] != NSNotFound) { [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; } haveShownXPCHelperErrorMessage = NO; // Set up audioDevices, which coordinates BGMDevice and the output device. It manages // playthrough, volume/mute controls, etc. if (![self initAudioDeviceManager]) { return; } // Stored user settings userDefaults = [self createUserDefaults]; // Add the status bar item. (The thing you click to show BGMApp's main menu.) statusBarItem = [[BGMStatusBarItem alloc] initWithMenu:self.bgmMenu audioDevices:audioDevices userDefaults:userDefaults]; } - (void) applicationDidFinishLaunching:(NSNotification*)aNotification { #pragma unused (aNotification) // Log the version/build number. // // TODO: NSLog should only be used for logging errors. // TODO: Automatically add the commit ID to the end of the build number for unreleased builds. (In the // Info.plist or something -- not here.) NSLog(@"BGMApp version: %@, BGMApp build number: %@", NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"], NSBundle.mainBundle.infoDictionary[@"CFBundleVersion"]); // Handles changing (or not changing) the output device when devices are added or removed. Must // be initialised before calling setBGMDeviceAsDefault. preferredOutputDevices = [[BGMPreferredOutputDevices alloc] initWithDevices:audioDevices userDefaults:userDefaults]; // 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. [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) { dispatch_async(dispatch_get_main_queue(), ^{ if (granted) { DebugMsg("BGMAppDelegate::applicationDidFinishLaunching: Permission granted"); [self continueLaunchAfterInputDevicePermissionGranted]; } else { NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: Permission denied"); // If they don't accept, Background Music won't work at all and the only way to // fix it is in System Preferences, so show an error dialog with instructions. // // TODO: It would be nice if this dialog had a shortcut to open the System // Preferences panel. See showSetDeviceAsDefaultError. [self showErrorMessage:@"Background Music needs permission to use microphones." informativeText:@"It uses a virtual microphone to access your system's " "audio.\n\nYou can grant the permission by going to " "System Preferences > Security and Privacy > " "Microphone and checking the box for Background Music." exitAfterMessageDismissed:YES]; } }); }]; } else #endif { // We can change the device immediately on older versions of macOS because they don't // require user permission for input devices. [self continueLaunchAfterInputDevicePermissionGranted]; } } - (void) continueLaunchAfterInputDevicePermissionGranted { // Choose an output device for BGMApp to use to play audio. if (![self setInitialOutputDevice]) { return; } // Make BGMDevice the default device. [self setBGMDeviceAsDefault]; // Handle some of the unusual reasons BGMApp might have to exit, mostly crashes. BGMTermination::SetUpTerminationCleanUp(audioDevices); // Set up the rest of the UI and other external interfaces. musicPlayers = [[BGMMusicPlayers alloc] initWithAudioDevices:audioDevices userDefaults:userDefaults]; autoPauseMusic = [[BGMAutoPauseMusic alloc] initWithAudioDevices:audioDevices musicPlayers:musicPlayers userDefaults:userDefaults]; [self setUpMainMenu]; xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices helperConnectionErrorHandler:^(NSError* error) { NSLog(@"BGMAppDelegate::continueLaunchAfterInputDevicePermissionGranted: " "(helperConnectionErrorHandler) BGMXPCHelper connection error: %@", error); [self showXPCHelperErrorMessage:error]; }]; } // Returns NO if (and only if) BGMApp is about to terminate because of a fatal error. - (BOOL) initAudioDeviceManager { audioDevices = [BGMAudioDeviceManager new]; if (!audioDevices) { [self showBGMDeviceNotFoundErrorMessageAndExit]; return NO; } return YES; } // Returns NO if (and only if) BGMApp is about to terminate because of a fatal error. - (BOOL) setInitialOutputDevice { AudioObjectID preferredDevice = [preferredOutputDevices findPreferredDevice]; if (preferredDevice != kAudioObjectUnknown) { NSError* __nullable error = [audioDevices setOutputDeviceWithID:preferredDevice revertOnFailure:NO]; if (error) { // Show the error message. [self showFailedToSetOutputDeviceErrorMessage:BGMNN(error) preferredDevice:preferredDevice]; } } else { // We couldn't find a device to use, so show an error message and quit. [self showOutputDeviceNotFoundErrorMessageAndExit]; return NO; } return YES; } // Sets the "Background Music" virtual audio device (BGMDevice) as the user's default audio device. - (void) setBGMDeviceAsDefault { 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."]; } } - (void) menuWillOpen:(NSMenu*)menu { if (@available(macOS 10.16, *)) { // Set menu offset and check for any active menu items float menuOffset = 12.0; for (NSMenuItem* menuItem in self.bgmMenu.itemArray) { if (menuItem.state == NSControlStateValueOn && menuItem.indentationLevel == 0) { menuOffset += 10; break; } } // Align volume output device and slider for (NSView* subview in self.outputVolumeView.subviews) { CGRect newSubview = subview.frame; newSubview.origin.x = menuOffset; subview.frame = newSubview; } // Align system sounds and app volumes double appIconTitleOffset = 0; for (NSMenuItem* menuItem in self.bgmMenu.itemArray) { if (menuItem.view.subviews.count == 7 || menuItem.view.subviews.count == 3) { NSTextField* appTitle; NSImageView* appIcon; for (NSView* subview in menuItem.view.subviews) { if (menuItem.view.subviews.count == 3) { // System sounds if ([subview isKindOfClass:[NSTextField class]]) { appTitle = (NSTextField*)subview; } if ([subview isKindOfClass:[NSImageView class]]) { appIcon = (NSImageView*)subview; } } else if (menuItem.view.subviews.count == 7) { // App volumes if ([subview isKindOfClass:[BGMAVM_AppNameLabel class]]) { appTitle = (NSTextField*)subview; } if ([subview isKindOfClass:[BGMAVM_AppIcon class]]) { appIcon = (NSImageView*)subview; } } } if (appIconTitleOffset == 0) { appIconTitleOffset = appTitle.frame.origin.x - appIcon.frame.origin.x; } CGRect newAppIcon = appIcon.frame; newAppIcon.origin.x = menuOffset; appIcon.frame = newAppIcon; CGRect newAppTitle = appTitle.frame; newAppTitle.origin.x = menuOffset + appIconTitleOffset; appTitle.frame = newAppTitle; } } } } - (void) setUpMainMenu { autoPauseMenuItem = [[BGMAutoPauseMenuItem alloc] initWithMenuItem:self.autoPauseMenuItemUnwrapped autoPauseMusic:autoPauseMusic musicPlayers:musicPlayers userDefaults:userDefaults]; [self initVolumesMenuSection]; // Output device selection. outputDeviceMenuSection = [[BGMOutputDeviceMenuSection alloc] initWithBGMMenu:self.bgmMenu audioDevices:audioDevices preferredDevices:preferredOutputDevices]; [audioDevices setOutputDeviceMenuSection:outputDeviceMenuSection]; // Preferences submenu. prefsMenu = [[BGMPreferencesMenu alloc] initWithBGMMenu:self.bgmMenu audioDevices:audioDevices musicPlayers:musicPlayers statusBarItem:statusBarItem aboutPanel:self.aboutPanel aboutPanelLicenseView:self.aboutPanelLicenseView userDefaults:userDefaults]; // Enable/disable debug logging. Hidden unless you option-click the status bar icon. debugLoggingMenuItem = [[BGMDebugLoggingMenuItem alloc] initWithMenuItem:self.debugLoggingMenuItemUnwrapped]; [statusBarItem setDebugLoggingMenuItem:debugLoggingMenuItem]; // Handle events about the main menu. (See the NSMenuDelegate methods below.) self.bgmMenu.delegate = self; } - (BGMUserDefaults*) createUserDefaults { 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) { [self showSetDeviceAsDefaultError:error message:@"Failed to reset your system's audio output device." informativeText:@"You'll have to change it yourself to get audio working again."]; } } #pragma mark Error messages - (void) showBGMDeviceNotFoundErrorMessageAndExit { // BGMDevice wasn't found on the system. Most likely, BGMDriver isn't installed. Show an error // dialog and exit. // // TODO: Check whether the driver files are in /Library/Audio/Plug-Ins/HAL? Might even want to // offer to install them if not. [self showErrorMessage:@"Could not find the Background Music virtual audio device." informativeText:@"Make sure you've installed Background Music Device.driver to " "/Library/Audio/Plug-Ins/HAL and restarted coreaudiod (e.g. \"sudo " "killall coreaudiod\")." exitAfterMessageDismissed:YES]; } - (void) showFailedToSetOutputDeviceErrorMessage:(NSError*)error preferredDevice:(BGMAudioDevice)device { NSLog(@"Failed to set initial output device. Error: %@", error); dispatch_async(dispatch_get_main_queue(), ^{ NSAlert* alert = [NSAlert alertWithError:BGMNN(error)]; alert.messageText = @"Failed to set the output device."; NSString* __nullable name = nil; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { name = (__bridge NSString* __nullable)device.CopyName(); }); alert.informativeText = [NSString stringWithFormat:@"Could not start the device '%@'. (Error: %ld)", name, error.code]; [alert runModal]; }); } - (void) showOutputDeviceNotFoundErrorMessageAndExit { // We couldn't find any output devices. Show an error dialog and exit. [self showErrorMessage:@"Could not find an audio output device." informativeText:@"If you do have one installed, this is probably a bug. Sorry about " "that. Feel free to file an issue on GitHub." exitAfterMessageDismissed:YES]; } - (void) showXPCHelperErrorMessage:(NSError*)error { if (!haveShownXPCHelperErrorMessage) { haveShownXPCHelperErrorMessage = YES; // NSAlert should only be used on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ NSAlert* alert = [NSAlert new]; // TODO: Offer to install BGMXPCHelper if it's missing. // TODO: Show suppression button? [alert setMessageText:@"Error connecting to BGMXPCHelper."]; [alert setInformativeText:[NSString stringWithFormat:@"%s%s%@ (%lu)", "Make sure you have BGMXPCHelper installed. There are instructions in the " "README.md file.\n\n" "Background Music might still work, but it won't work as well as it could.", "\n\nDetails:\n", [error localizedDescription], [error code]]]; [alert runModal]; }); } } - (void) showErrorMessage:(NSString*)message informativeText:(NSString*)informativeText exitAfterMessageDismissed:(BOOL)fatal { // NSAlert should only be used on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ NSAlert* alert = [NSAlert new]; [alert setMessageText:message]; [alert setInformativeText:informativeText]; // This crashes if built with Xcode 9.0.1, but works with versions of Xcode before 9 and // with 9.1. [alert runModal]; if (fatal) { [NSApp terminate:self]; } }); } - (void) showSetDeviceAsDefaultError:(NSError*)error message:(NSString*)msg informativeText:(NSString*)info { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"%@ %@ Error: %@", msg, info, error); NSAlert* alert = [NSAlert alertWithError:error]; alert.messageText = msg; alert.informativeText = info; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Open Sound in System Preferences"]; NSModalResponse buttonClicked = [alert runModal]; if (buttonClicked != NSAlertFirstButtonReturn) { // 'OK' is the first button. [self openSysPrefsSoundOutput]; } }); } - (void) openSysPrefsSoundOutput { SystemPreferencesApplication* __nullable sysPrefs = [SBApplication applicationWithBundleIdentifier:@"com.apple.systempreferences"]; if (!sysPrefs) { NSLog(@"Could not open System Preferences"); return; } // In System Preferences, go to the "Output" tab on the "Sound" pane. for (SystemPreferencesPane* pane : [sysPrefs panes]) { DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: pane = %s", [pane.name UTF8String]); if ([pane.id isEqualToString:@"com.apple.preference.sound"]) { sysPrefs.currentPane = pane; for (SystemPreferencesAnchor* anchor : [pane anchors]) { DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: anchor = %s", [anchor.name UTF8String]); if ([[anchor.name lowercaseString] isEqualToString:@"output"]) { DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: Showing Output in Sound pane."); [anchor reveal]; } } } } // Bring System Preferences to the foreground. [sysPrefs activate]; } #pragma mark NSMenuDelegate - (void) menuNeedsUpdate:(NSMenu*)menu { if ([menu isEqual:self.bgmMenu]) { [autoPauseMenuItem parentMenuNeedsUpdate]; } else { DebugMsg("BGMAppDelegate::menuNeedsUpdate: Warning: unexpected menu. menu=%s", menu.description.UTF8String); } } - (void) menu:(NSMenu*)menu willHighlightItem:(NSMenuItem* __nullable)item { if ([menu isEqual:self.bgmMenu]) { [autoPauseMenuItem parentMenuItemWillHighlight:item]; } else { DebugMsg("BGMAppDelegate::menu: Warning: unexpected menu. menu=%s", menu.description.UTF8String); } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAppVolumes.h ================================================ // 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 . // // BGMAppVolumes.h // BGMApp // // Copyright © 2016, 2017 Kyle Neideck // Copyright © 2021 Marcus Wu // Copyright © 2026 TwelfthFace // // Local Includes #import "BGMAppVolumesController.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMAppVolumes : NSObject - (id) initWithController:(BGMAppVolumesController*)inController bgmMenu:(NSMenu*)inMenu appVolumeView:(NSView*)inView; // Pass -1 for initialVolume or kAppPanNoValue for 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; - (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app; - (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app; @end // Protocol for the UI custom classes @protocol BGMAppVolumeMenuItemSubview - (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx controller:(BGMAppVolumesController*)ctrl menuItem:(NSMenuItem*)item; @end // Custom classes for the UI elements in the app volume menu items @interface BGMAVM_VolumeMute: NSButton - (void) bgm_syncForVolume:(int)vol; @end @interface BGMAVM_AppIcon : NSImageView @end @interface BGMAVM_AppNameLabel : NSTextField @end @interface BGMAVM_ShowMoreControlsButton : NSButton @end @interface BGMAVM_VolumeSlider : NSSlider - (void) setRelativeVolume:(int)relativeVolume; @end @interface BGMAVM_PanSlider : NSSlider - (void) setPanPosition:(int)panPosition; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAppVolumes.m ================================================ // 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 . // // BGMAppVolumes.m // BGMApp // // Copyright © 2016-2020, 2026 Kyle Neideck // Copyright © 2017 Andrew Tonner // Copyright © 2021 Marcus Wu // Copyright © 2022 Jon Egan // Copyright © 2026 TwelfthFace // // 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*)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]; } } - (NSMenuItem*) getMenuItemForApp:(NSRunningApplication*)app { NSInteger lastAppVolumeMenuItemIndex = [self lastMenuItemIndex] - 2; 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]) { return item; } } 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]) { return item; } } return nil; } - (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app { BGMAppVolumeAndPan result = { .volume = -1, .pan = kAppPanNoValue }; NSMenuItem *item = [self getMenuItemForApp:app]; if (item == nil) { return result; } for (NSView* subview in item.view.subviews) { // Get the volume. if ([subview isKindOfClass:[BGMAVM_VolumeSlider class]]) { result.volume = [(BGMAVM_VolumeSlider*)subview intValue]; } // Get the pan position. if ([subview isKindOfClass:[BGMAVM_PanSlider class]]) { result.pan = [(BGMAVM_PanSlider*)subview intValue]; } } return result; } - (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app { NSMenuItem *item = [self getMenuItemForApp:app]; if (item == nil) { return; } for (NSView* subview in item.view.subviews) { // Set the volume. if (volumeAndPan.volume != -1 && [subview isKindOfClass:[BGMAVM_VolumeSlider class]]) { [(BGMAVM_VolumeSlider*)subview setRelativeVolume:volumeAndPan.volume]; } // Set the pan position. if (volumeAndPan.pan != kAppPanNoValue && [subview isKindOfClass:[BGMAVM_PanSlider class]]) { [(BGMAVM_PanSlider*)subview setPanPosition:volumeAndPan.pan]; } } } // 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 != kAppPanNoValue && [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; #if DEBUG CGFloat height = menuItem.view.frame.size.height; #endif const char* appName = [((NSRunningApplication*)menuItem.representedObject).localizedName UTF8String]; // Using this function (instead of just ==) shouldn't be necessary, but just in case. #if DEBUG BOOL(^nearEnough)(CGFloat x, CGFloat y) = ^BOOL(CGFloat x, CGFloat y) { return fabs(x - y) < 0.01; // We don't need much precision. }; #endif bool allSubviewsShowing = true; for (NSView* subview in menuItem.view.subviews) { if (subview.hidden) { allSubviewsShowing = false; break; } //DebugMsg("BGMAppVolumes:: subview hash / hidden: (%lu) / (%hhd)", (unsigned long)subview.hash, subview.hidden); } if (allSubviewsShowing) { // 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 its 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_VolumeMute { pid_t appProcessID; NSString* __nullable appBundleID; BGMAppVolumesController* controller; } - (NSString*) lastNonZeroVolumeDefaultsKey { if (appBundleID.length > 0) { return [NSString stringWithFormat:@"BGMAVM_LastNonZeroVolume_%@", appBundleID]; } return [NSString stringWithFormat:@"BGMAVM_LastNonZeroVolume_pid_%d", appProcessID]; } - (BOOL) isMuted:(int)value { return value <= kAppRelativeVolumeMinRawValue; } - (int) defaultRestoreVolume { return (int)((kAppRelativeVolumeMaxRawValue + kAppRelativeVolumeMinRawValue) / 2); } - (BGMAVM_VolumeSlider* __nullable) findSiblingVolumeSlider { for (NSView* view in self.superview.subviews) { if ([view isKindOfClass:[BGMAVM_VolumeSlider class]]) { return (BGMAVM_VolumeSlider*)view; } } return nil; } - (void) updateButtonForVolume:(int)volume { BOOL muted = [self isMuted:volume]; if ([NSImage respondsToSelector:@selector(imageWithSystemSymbolName:accessibilityDescription:)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" NSString* symbol = muted ? @"speaker.slash.fill" : @"speaker.wave.2.fill"; NSString* description = muted ? @"Unmute" : @"Mute"; self.image = [NSImage imageWithSystemSymbolName:symbol accessibilityDescription:description]; #pragma clang diagnostic pop self.imagePosition = NSImageOnly; self.title = @""; } else { self.title = muted ? @"Unmute" : @"Mute"; } } - (void) bgm_syncForVolume:(int)volume { [self updateButtonForVolume:volume]; } - (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx controller:(BGMAppVolumesController*)ctrl menuItem:(NSMenuItem*)menuItem { #pragma unused (ctx, menuItem) controller = ctrl; appProcessID = app.processIdentifier; appBundleID = app.bundleIdentifier; self.target = self; self.action = @selector(mutePressed:); BGMAVM_VolumeSlider* slider = [self findSiblingVolumeSlider]; int currentVol = slider ? slider.intValue : kAppRelativeVolumeMinRawValue; [self updateButtonForVolume:currentVol]; } - (IBAction) mutePressed:(id)sender { #pragma unused(sender) BGMAVM_VolumeSlider* slider = [self findSiblingVolumeSlider]; if (!slider) { DebugMsg("Mute button: no slider found"); return; } int currentVol = slider.intValue; BOOL mutedNow = [self isMuted:currentVol]; if (!mutedNow) { // Store last volume [[NSUserDefaults standardUserDefaults] setInteger:currentVol forKey:[self lastNonZeroVolumeDefaultsKey]]; [slider setRelativeVolume:kAppRelativeVolumeMinRawValue]; } else { NSInteger last = [[NSUserDefaults standardUserDefaults] integerForKey:[self lastNonZeroVolumeDefaultsKey]]; int restoreVol = (int)last; if (restoreVol <= kAppRelativeVolumeMinRawValue || restoreVol > kAppRelativeVolumeMaxRawValue) { restoreVol = [self defaultRestoreVolume]; } [slider setRelativeVolume:restoreVol]; } [controller setVolume:slider.intValue forAppWithProcessID:appProcessID bundleID:appBundleID]; [self updateButtonForVolume:slider.intValue]; } @end @implementation BGMAVM_VolumeSlider { // Will be set to -1 for apps without a pid pid_t appProcessID; NSString* __nullable appBundleID; BGMAppVolumesController* controller; // Keep the menu item so we can sync the mute button when the slider changes. __weak NSMenuItem* menuItem; } - (void) setUpWithApp:(NSRunningApplication*)app context:(BGMAppVolumes*)ctx controller:(BGMAppVolumesController*)ctrl menuItem:(NSMenuItem*)inMenuItem { #pragma unused (ctx) controller = ctrl; menuItem = inMenuItem; 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]; // Sync the mute button so it reflects muted/unmuted when the user drags the slider. for (NSView* subview in menuItem.view.subviews) { if ([subview isKindOfClass:[BGMAVM_VolumeMute class]]) { [(BGMAVM_VolumeMute*)subview bgm_syncForVolume:self.intValue]; } } } @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 ================================================ FILE: BGMApp/BGMApp/BGMAppVolumesController.h ================================================ // 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 . // // BGMAppVolumesController.h // BGMApp // // Copyright © 2017 Kyle Neideck // Copyright © 2021 Marcus Wu // // Local Includes #import "BGMAudioDeviceManager.h" // System Includes #import #pragma clang assume_nonnull begin typedef struct BGMAppVolumeAndPan { int volume; int pan; } BGMAppVolumeAndPan; @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; - (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication *)app; - (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAppVolumesController.mm ================================================ // 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 . // // BGMAppVolumesController.mm // BGMApp // // Copyright © 2017, 2018 Kyle Neideck // Copyright © 2017 Andrew Tonner // Copyright © 2021 Marcus Wu // // 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 #pragma clang assume_nonnull begin @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* 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*)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 { return [appVolumes getVolumeAndPanForApp:app]; } - (void) setVolumeAndPan:(BGMAppVolumeAndPan)volumeAndPan forApp:(NSRunningApplication*)app { [appVolumes setVolumeAndPan:volumeAndPan forApp:app]; if (volumeAndPan.volume != -1) { [self setVolume:volumeAndPan.volume forAppWithProcessID:app.processIdentifier bundleID:app.bundleIdentifier]; } if (volumeAndPan.pan != kAppPanNoValue) { [self setPanPosition:volumeAndPan.pan forAppWithProcessID:app.processIdentifier bundleID:app.bundleIdentifier]; } } - (BGMAppVolumeAndPan) getVolumeAndPanForApp:(NSRunningApplication*)app fromVolumes:(const CACFArray&)volumes { BGMAppVolumeAndPan volumeAndPan = { .volume = -1, .pan = kAppPanNoValue }; 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*)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* newApps = change[NSKeyValueChangeNewKey]; NSArray* 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 ================================================ FILE: BGMApp/BGMApp/BGMAppWatcher.h ================================================ // 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 . // // BGMAppWatcher.h // BGMApp // // Copyright © 2019 Kyle Neideck // // Calls callback functions when a given application is launched or terminated. Starts watching // after being initialised, stops after being destroyed. // // System Includes #import #pragma clang assume_nonnull begin @interface BGMAppWatcher : NSObject // appLaunched will be called when the application is launched and appTerminated will be called when // it's terminated. Background apps, status bar apps, etc. are ignored. - (instancetype) initWithBundleID:(NSString*)bundleID appLaunched:(void(^)(void))appLaunched appTerminated:(void(^)(void))appTerminated; // With this constructor, when an application is launched or terminated, isMatchingBundleID will be // called first to decide whether or not the callback should be called. - (instancetype) initWithAppLaunched:(void(^)(void))appLaunched appTerminated:(void(^)(void))appTerminated isMatchingBundleID:(BOOL(^)(NSString* appBundleID))isMatchingBundleID; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAppWatcher.m ================================================ // 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 . // // BGMAppWatcher.m // BGMApp // // Copyright © 2019 Kyle Neideck // // Self Include #import "BGMAppWatcher.h" // System Includes #import #pragma clang assume_nonnull begin @implementation BGMAppWatcher { // Tokens for the notification observers so we can remove them in dealloc. id didLaunchToken; id didTerminateToken; } - (instancetype) initWithBundleID:(NSString*)bundleID appLaunched:(void(^)(void))appLaunched appTerminated:(void(^)(void))appTerminated { return [self initWithAppLaunched:appLaunched appTerminated:appTerminated isMatchingBundleID:^BOOL(NSString* appBundleID) { return [bundleID isEqualToString:appBundleID]; }]; } - (instancetype) initWithAppLaunched:(void(^)(void))appLaunched appTerminated:(void(^)(void))appTerminated isMatchingBundleID:(BOOL(^)(NSString*))isMatchingBundleID { if ((self = [super init])) { NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter; didLaunchToken = [center addObserverForName:NSWorkspaceDidLaunchApplicationNotification object:nil queue:nil usingBlock:^(NSNotification* notification) { if ([BGMAppWatcher shouldBeHandled:notification isMatchingBundleID:isMatchingBundleID]) { appLaunched(); } }]; didTerminateToken = [center addObserverForName:NSWorkspaceDidTerminateApplicationNotification object:nil queue:nil usingBlock:^(NSNotification* notification) { if ([BGMAppWatcher shouldBeHandled:notification isMatchingBundleID:isMatchingBundleID]) { appTerminated(); } }]; } return self; } // Returns YES if we should call the app launch/termination callback for this NSNotification. + (BOOL) shouldBeHandled:(NSNotification*)notification isMatchingBundleID:(BOOL(^)(NSString*))isMatchingBundleID { NSString* __nullable notifiedBundleID = [notification.userInfo[NSWorkspaceApplicationKey] bundleIdentifier]; // Ignore the notification if the app doesn't have a bundle ID or isMatchingBundleID returns NO. return notifiedBundleID && isMatchingBundleID((NSString*)notifiedBundleID); } - (void) dealloc { // Remove the application launch/termination observers. NSNotificationCenter* center = [NSWorkspace sharedWorkspace].notificationCenter; if (didLaunchToken) { [center removeObserver:didLaunchToken]; didLaunchToken = nil; } if (didTerminateToken) { [center removeObserver:didTerminateToken]; didTerminateToken = nil; } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAudioDevice.cpp ================================================ // 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 . // // BGMAudioDevice.cpp // BGMApp // // Copyright © 2017 Kyle Neideck // // Self Include #include "BGMAudioDevice.h" // Local Includes #include "BGM_Types.h" // System Includes #include #pragma mark Construction/Destruction BGMAudioDevice::BGMAudioDevice(AudioObjectID inAudioDevice) : CAHALAudioDevice(inAudioDevice) { } BGMAudioDevice::BGMAudioDevice(CFStringRef inUID) : CAHALAudioDevice(inUID) { } BGMAudioDevice::BGMAudioDevice(const CAHALAudioDevice& inDevice) : BGMAudioDevice(inDevice.GetObjectID()) { } BGMAudioDevice::~BGMAudioDevice() { } bool BGMAudioDevice::CanBeOutputDeviceInBGMApp() const { CFStringRef uid = CopyDeviceUID(); assert(uid != nullptr); bool isNullDevice = CFEqual(uid, CFSTR(kBGMNullDeviceUID)); CFRelease(uid); bool hasOutputChannels = GetTotalNumberChannels(/* inIsInput = */ false) > 0; bool canBeDefault = CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false); return !IsBGMDeviceInstance() && !isNullDevice && !IsHidden() && hasOutputChannels && canBeDefault; } #pragma mark Available Controls bool BGMAudioDevice::HasSettableMasterVolume(AudioObjectPropertyScope inScope) const { return HasVolumeControl(inScope, kMasterChannel) && VolumeControlIsSettable(inScope, kMasterChannel); } bool BGMAudioDevice::HasSettableVirtualMasterVolume(AudioObjectPropertyScope inScope) const { AudioObjectPropertyAddress virtualMasterVolumeAddress = { kAudioHardwareServiceDeviceProperty_VirtualMainVolume, inScope, kAudioObjectPropertyElementMaster }; // TODO: Replace these calls deprecated AudioToolbox functions. There are more below. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" Boolean virtualMasterVolumeIsSettable; OSStatus err = AudioHardwareServiceIsPropertySettable(GetObjectID(), &virtualMasterVolumeAddress, &virtualMasterVolumeIsSettable); virtualMasterVolumeIsSettable &= (err == kAudioServicesNoError); bool hasVirtualMasterVolume = AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterVolumeAddress); #pragma clang diagnostic pop return hasVirtualMasterVolume && virtualMasterVolumeIsSettable; } bool BGMAudioDevice::HasSettableMasterMute(AudioObjectPropertyScope inScope) const { return HasMuteControl(inScope, kMasterChannel) && MuteControlIsSettable(inScope, kMasterChannel); } #pragma mark Control Values Accessors void BGMAudioDevice::CopyMuteFrom(const BGMAudioDevice inDevice, AudioObjectPropertyScope inScope) { // TODO: Support for devices that have per-channel mute controls but no master mute control if(HasSettableMasterMute(inScope) && inDevice.HasMuteControl(inScope, kMasterChannel)) { SetMuteControlValue(inScope, kMasterChannel, inDevice.GetMuteControlValue(inScope, kMasterChannel)); } } void BGMAudioDevice::CopyVolumeFrom(const BGMAudioDevice inDevice, AudioObjectPropertyScope inScope) { // Get the volume of the other device. bool didGetVolume = false; Float32 volume = FLT_MIN; if(inDevice.HasVolumeControl(inScope, kMasterChannel)) { volume = inDevice.GetVolumeControlScalarValue(inScope, kMasterChannel); didGetVolume = true; } // Use the average channel volume of the other device if it has no master volume. if(!didGetVolume) { UInt32 numChannels = inDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput); volume = 0; for(UInt32 channel = 1; channel <= numChannels; channel++) { if(inDevice.HasVolumeControl(inScope, channel)) { volume += inDevice.GetVolumeControlScalarValue(inScope, channel); didGetVolume = true; } } if(numChannels > 0) // Avoid divide by zero. { volume /= numChannels; } } // Set the volume of this device. if(didGetVolume && volume != FLT_MIN) { bool didSetVolume = false; try { didSetVolume = SetMasterVolumeScalar(inScope, volume); } catch(CAException e) { OSStatus err = e.GetError(); char err4CC[5] = CA4CCToCString(err); CFStringRef uid = CopyDeviceUID(); LogWarning("BGMAudioDevice::CopyVolumeFrom: CAException '%s' trying to set master " "volume of %s", err4CC, CFStringGetCStringPtr(uid, kCFStringEncodingUTF8)); CFRelease(uid); } if(!didSetVolume) { // Couldn't find a master volume control to set, so try to find a virtual one Float32 virtualMasterVolume; bool success = inDevice.GetVirtualMasterVolumeScalar(inScope, virtualMasterVolume); if(success) { didSetVolume = SetVirtualMasterVolumeScalar(inScope, virtualMasterVolume); } } if(!didSetVolume) { // Couldn't set a master or virtual master volume, so as a fallback try to set each // channel individually. UInt32 numChannels = GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput); for(UInt32 channel = 1; channel <= numChannels; channel++) { if(HasVolumeControl(inScope, channel) && VolumeControlIsSettable(inScope, channel)) { SetVolumeControlScalarValue(inScope, channel, volume); } } } } } bool BGMAudioDevice::SetMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume) { if(HasSettableMasterVolume(inScope)) { SetVolumeControlScalarValue(inScope, kMasterChannel, inVolume); return true; } return false; } bool BGMAudioDevice::GetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32& outVirtualMasterVolume) const { AudioObjectPropertyAddress virtualMasterVolumeAddress = { kAudioHardwareServiceDeviceProperty_VirtualMainVolume, inScope, kAudioObjectPropertyElementMaster }; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if(!AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterVolumeAddress)) { return false; } #pragma clang diagnostic pop UInt32 virtualMasterVolumePropertySize = sizeof(Float32); return kAudioServicesNoError == AHSGetPropertyData(GetObjectID(), &virtualMasterVolumeAddress, &virtualMasterVolumePropertySize, &outVirtualMasterVolume); } bool BGMAudioDevice::SetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume) { // TODO: For me, setting the virtual master volume sets all the device's channels to the same volume, meaning you can't // keep any channels quieter than the others. The expected behaviour is to scale the channel volumes // proportionally. So to do this properly I think we'd have to store BGMDevice's previous volume and calculate // each channel's new volume from its current volume and the distance between BGMDevice's old and new volumes. // // The docs kAudioHardwareServiceDeviceProperty_VirtualMasterVolume for say // "If the device has individual channel volume controls, this property will apply to those identified by the // device's preferred multi-channel layout (or preferred stereo pair if the device is stereo only). Note that // this control maintains the relative balance between all the channels it affects. // so I'm not sure why that's not working here. As a workaround we take the to device's (virtual master) balance // before changing the volume and set it back after, but of course that'll only work for stereo devices. bool didSetVolume = false; if(HasSettableVirtualMasterVolume(inScope)) { // Not sure why, but setting the virtual master volume sets all channels to the same volume. As a workaround, we store // the current balance here so we can reset it after setting the volume. Float32 virtualMasterBalance; bool didGetVirtualMasterBalance = GetVirtualMasterBalance(inScope, virtualMasterBalance); AudioObjectPropertyAddress virtualMasterVolumeAddress = { kAudioHardwareServiceDeviceProperty_VirtualMainVolume, inScope, kAudioObjectPropertyElementMaster }; didSetVolume = (kAudioServicesNoError == AHSSetPropertyData(GetObjectID(), &virtualMasterVolumeAddress, sizeof(Float32), &inVolume)); // Reset the balance AudioObjectPropertyAddress virtualMasterBalanceAddress = { kAudioHardwareServiceDeviceProperty_VirtualMainBalance, inScope, kAudioObjectPropertyElementMaster }; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if(didSetVolume && didGetVirtualMasterBalance && AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterBalanceAddress)) { Boolean balanceIsSettable; OSStatus err = AudioHardwareServiceIsPropertySettable(GetObjectID(), &virtualMasterBalanceAddress, &balanceIsSettable); if(err == kAudioServicesNoError && balanceIsSettable) { AHSSetPropertyData(GetObjectID(), &virtualMasterBalanceAddress, sizeof(Float32), &virtualMasterBalance); } } #pragma clang diagnostic pop } return didSetVolume; } bool BGMAudioDevice::GetVirtualMasterBalance(AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance) const { AudioObjectPropertyAddress virtualMasterBalanceAddress = { kAudioHardwareServiceDeviceProperty_VirtualMainBalance, inScope, kAudioObjectPropertyElementMaster }; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if(!AudioHardwareServiceHasProperty(GetObjectID(), &virtualMasterBalanceAddress)) { return false; } #pragma clang diagnostic pop UInt32 virtualMasterVolumePropertySize = sizeof(Float32); return kAudioServicesNoError == AHSGetPropertyData(GetObjectID(), &virtualMasterBalanceAddress, &virtualMasterVolumePropertySize, &outVirtualMasterBalance); } #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(); if (uid == nullptr) { return isBGMDevice; } isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID)) || (inIncludeUISoundsInstance && CFEqual(uid, CFSTR(kBGMDeviceUID_UISounds))); CFRelease(uid); } return isBGMDevice; } // static OSStatus BGMAudioDevice::AHSGetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32* ioDataSize, void* outData) { // The docs for AudioHardwareServiceGetPropertyData specifically allow passing NULL for // inQualifierData as we do here, but it's declared in an assume_nonnull section so we have to // disable the warning here. I'm not sure why inQualifierData isn't __nullable. I'm assuming // it's either a backwards compatibility thing or just a bug. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" #pragma clang diagnostic ignored "-Wdeprecated-declarations" // The non-depreciated version of this (and the setter below) doesn't seem to support devices // other than the default return AudioHardwareServiceGetPropertyData(inObjectID, inAddress, 0, NULL, ioDataSize, outData); #pragma clang diagnostic pop } // static OSStatus BGMAudioDevice::AHSSetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inDataSize, const void* inData) { // See the explanation about these pragmas in AHSGetPropertyData #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnonnull" #pragma clang diagnostic ignored "-Wdeprecated-declarations" return AudioHardwareServiceSetPropertyData(inObjectID, inAddress, 0, NULL, inDataSize, inData); #pragma clang diagnostic pop } ================================================ FILE: BGMApp/BGMApp/BGMAudioDevice.h ================================================ // 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 . // BGMAudioDevice.h // BGMApp // // Copyright © 2017, 2020 Kyle Neideck // // A HAL audio device. Note that this class's only state is the AudioObjectID of the device. // #ifndef BGMApp__BGMAudioDevice #define BGMApp__BGMAudioDevice // PublicUtility Includes #include "CAHALAudioDevice.h" class BGMAudioDevice : public CAHALAudioDevice { #pragma mark Construction/Destruction public: BGMAudioDevice(AudioObjectID inAudioDevice); /*! 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(); #if defined(__OBJC__) // Hack/workaround for Objective-C classes so we don't have to use pointers for instance // variables. BGMAudioDevice() : BGMAudioDevice(kAudioObjectUnknown) { } #endif /* defined(__OBJC__) */ operator AudioObjectID() const { return GetObjectID(); } /*! @return True if this device is BGMDevice. (Specifically, the main instance of BGMDevice, not the instance used for UI sounds.) @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 bool HasSettableMasterVolume(AudioObjectPropertyScope inScope) const; bool HasSettableVirtualMasterVolume(AudioObjectPropertyScope inScope) const; bool HasSettableMasterMute(AudioObjectPropertyScope inScope) const; #pragma mark Control Values Accessors void CopyMuteFrom(const BGMAudioDevice inDevice, AudioObjectPropertyScope inScope); void CopyVolumeFrom(const BGMAudioDevice inDevice, AudioObjectPropertyScope inScope); bool SetMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume); bool GetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32& outVirtualMasterVolume) const; bool SetVirtualMasterVolumeScalar(AudioObjectPropertyScope inScope, Float32 inVolume); bool GetVirtualMasterBalance(AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance) const; #pragma mark Implementation private: bool IsBGMDevice(bool inIncludingUISoundsInstance) const; static OSStatus AHSGetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32* ioDataSize, void* outData); static OSStatus AHSSetPropertyData(AudioObjectID inObjectID, const AudioObjectPropertyAddress* inAddress, UInt32 inDataSize, const void* inData); }; #endif /* BGMApp__BGMAudioDevice */ ================================================ FILE: BGMApp/BGMApp/BGMAudioDeviceManager.h ================================================ // 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 . // // BGMAudioDeviceManager.h // BGMApp // // Copyright © 2016-2018 Kyle Neideck // // Manages BGMDevice and the output device. Sets the system's current default device as the output // device on init, then starts playthrough and mirroring the devices' controls. // #if defined(__cplusplus) // Local Includes #import "BGMBackgroundMusicDevice.h" // PublicUtility Includes #import "CAHALAudioDevice.h" #endif /* defined(__cplusplus) */ // System Includes #import #import // Forward Declarations @class BGMOutputVolumeMenuItem; @class BGMOutputDeviceMenuSection; #pragma clang assume_nonnull begin static const int kBGMErrorCode_OutputDeviceNotFound = 1; static const int kBGMErrorCode_ReturningEarly = 2; @interface BGMAudioDeviceManager : NSObject // Returns nil if BGMDevice isn't installed. - (instancetype) init; // Set the BGMOutputVolumeMenuItem to be notified when the output device is changed. - (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item; // Set the BGMOutputDeviceMenuSection to be notified when the output device is changed. - (void) setOutputDeviceMenuSection:(BGMOutputDeviceMenuSection*)menuSection; // Set BGMDevice as the default audio device for all processes - (NSError* __nullable) setBGMDeviceAsOSDefault; // Replace BGMDevice as the default device with the output device - (NSError* __nullable) unsetBGMDeviceAsOSDefault; #ifdef __cplusplus // The virtual device published by BGMDriver. - (BGMBackgroundMusicDevice) bgmDevice; // The device BGMApp will play audio through, making it, from the user's perspective, the system's // default output device. - (CAHALAudioDevice) outputDevice; #endif - (BOOL) isOutputDevice:(AudioObjectID)deviceID; - (BOOL) isOutputDataSource:(UInt32)dataSourceID; // Set the audio output device that BGMApp uses. // // Returns an error if the output device couldn't be changed. If revertOnFailure is true in that case, // this method will attempt to set the output device back to the original device. If it fails to // revert, an additional error will be included in the error's userInfo with the key "revertError". // // Both errors' codes will be the code of the exception that caused the failure, if any, generally one // of the error constants from AudioHardwareBase.h. // // Blocks while the old device stops IO (if there was one). - (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID revertOnFailure:(BOOL)revertOnFailure; // As above, but also sets the new output device's data source. See kAudioDevicePropertyDataSource in // AudioHardware.h. - (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID dataSourceID:(UInt32)dataSourceID revertOnFailure:(BOOL)revertOnFailure; // 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 #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAudioDeviceManager.mm ================================================ // 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 . // // BGMAudioDeviceManager.mm // BGMApp // // Copyright © 2016-2018 Kyle Neideck // // Self Include #import "BGMAudioDeviceManager.h" // Local Includes #import "BGM_Types.h" #import "BGM_Utils.h" #import "BGMAudioDevice.h" #import "BGMDeviceControlSync.h" #import "BGMOutputDeviceMenuSection.h" #import "BGMOutputVolumeMenuItem.h" #import "BGMPlayThrough.h" #import "BGMXPCProtocols.h" // PublicUtility Includes #import "CAAtomic.h" #import "CAAutoDisposer.h" #import "CAHALAudioSystemObject.h" #pragma clang assume_nonnull begin @implementation BGMAudioDeviceManager { // 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; BGMOutputDeviceMenuSection* __nullable outputDeviceMenuSection; NSRecursiveLock* stateLock; } #pragma mark Construction/Destruction - (instancetype) init { if ((self = [super init])) { stateLock = [NSRecursiveLock new]; bgmXPCHelperConnection = nil; outputVolumeMenuItem = nil; outputDeviceMenuSection = nil; outputDevice = kAudioObjectUnknown; try { bgmDevice = new BGMBackgroundMusicDevice; } catch (const CAException& e) { LogError("BGMAudioDeviceManager::init: BGMDevice not found. (%d)", e.GetError()); self = nil; return self; } } return self; } - (void) dealloc { @try { [stateLock lock]; if (bgmDevice) { delete bgmDevice; bgmDevice = nullptr; } } @finally { [stateLock unlock]; } } - (void) setOutputVolumeMenuItem:(BGMOutputVolumeMenuItem*)item { outputVolumeMenuItem = item; } - (void) setOutputDeviceMenuSection:(BGMOutputDeviceMenuSection*)menuSection { outputDeviceMenuSection = menuSection; } #pragma mark Systemwide Default Device // Note that there are two different "default" output devices on OS X: "output" and "system output". See // kAudioHardwarePropertyDefaultSystemOutputDevice in AudioHardware.h. - (NSError* __nullable) setBGMDeviceAsOSDefault { try { // 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 { // Copy the devices so we can call the HAL without holding stateLock. See startPlayThroughSync. BGMBackgroundMusicDevice* bgmDeviceCopy; AudioDeviceID outputDeviceID; @try { [stateLock lock]; bgmDeviceCopy = bgmDevice; outputDeviceID = outputDevice.GetObjectID(); } @finally { [stateLock unlock]; } if (outputDeviceID == kAudioObjectUnknown) { return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil]; } try { bgmDeviceCopy->UnsetAsOSDefault(outputDeviceID); } catch (const CAException& e) { BGMLogExceptionIn("BGMAudioDeviceManager::unsetBGMDeviceAsOSDefault", e); return [NSError errorWithDomain:@kBGMAppBundleID code:e.GetError() userInfo:nil]; } return nil; } #pragma mark Accessors - (BGMBackgroundMusicDevice) bgmDevice { return *bgmDevice; } - (CAHALAudioDevice) outputDevice { return outputDevice; } - (BOOL) isOutputDevice:(AudioObjectID)deviceID { @try { [stateLock lock]; return deviceID == outputDevice.GetObjectID(); } @finally { [stateLock unlock]; } } - (BOOL) isOutputDataSource:(UInt32)dataSourceID { BOOL isOutputDataSource = NO; @try { [stateLock lock]; try { AudioObjectPropertyScope scope = kAudioDevicePropertyScopeOutput; UInt32 channel = 0; isOutputDataSource = outputDevice.HasDataSourceControl(scope, channel) && (dataSourceID == outputDevice.GetCurrentDataSourceID(scope, channel)); } catch (const CAException& e) { BGMLogException(e); } } @finally { [stateLock unlock]; } return isOutputDataSource; } #pragma mark Output Device - (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID revertOnFailure:(BOOL)revertOnFailure { return [self setOutputDeviceWithIDImpl:deviceID dataSourceID:nil revertOnFailure:revertOnFailure]; } - (NSError* __nullable) setOutputDeviceWithID:(AudioObjectID)deviceID dataSourceID:(UInt32)dataSourceID revertOnFailure:(BOOL)revertOnFailure { return [self setOutputDeviceWithIDImpl:deviceID dataSourceID:&dataSourceID revertOnFailure:revertOnFailure]; } - (NSError* __nullable) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID dataSourceID:(UInt32* __nullable)dataSourceID revertOnFailure:(BOOL)revertOnFailure { DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Setting output device. newDeviceID=%u", newDeviceID); @try { [stateLock lock]; AudioDeviceID currentDeviceID = outputDevice.GetObjectID(); // (Doesn't throw.) try { [self setOutputDeviceWithIDImpl:newDeviceID dataSourceID:dataSourceID currentDeviceID:currentDeviceID]; } catch (const CAException& e) { BGMAssert(e.GetError() != kAudioHardwareNoError, "CAException with kAudioHardwareNoError"); return [self failedToSetOutputDevice:newDeviceID errorCode:e.GetError() revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)]; } catch (...) { return [self failedToSetOutputDevice:newDeviceID errorCode:kAudioHardwareUnspecifiedError revertTo:(revertOnFailure ? ¤tDeviceID : nullptr)]; } // Tell other classes and BGMXPCHelper that we changed the output device. [self propagateOutputDeviceChange]; } @finally { [stateLock unlock]; } return nil; } // Throws CAException. - (void) setOutputDeviceWithIDImpl:(AudioObjectID)newDeviceID dataSourceID:(UInt32* __nullable)dataSourceID currentDeviceID:(AudioObjectID)currentDeviceID { if (newDeviceID != currentDeviceID) { BGMAudioDevice newOutputDevice(newDeviceID); [self setOutputDeviceForPlaythroughAndControlSync:newOutputDevice]; outputDevice = newOutputDevice; } // Set the output device to use the new data source. if (dataSourceID) { // TODO: If this fails, ideally we'd still start playthrough and return an error, but not // revert the device. It would probably be a bit awkward, though. [self setDataSource:*dataSourceID device:outputDevice]; } if (newDeviceID != currentDeviceID) { // We successfully changed to the new device. Start playthrough on it, since audio might be // playing. (If we only changed the data source, playthrough will already be running if it // needs to be.) playThrough.Start(); playThrough_UISounds.Start(); // But stop playthrough if audio isn't playing, since it uses CPU. playThrough.StopIfIdle(); playThrough_UISounds.StopIfIdle(); } CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID(); DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithIDImpl: Set output device to %s (%d)", CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8), outputDevice.GetObjectID()); CFRelease(outputDeviceUID); } // Changes the output device that playthrough plays audio to and that BGMDevice's controls are // kept in sync with. Throws CAException. - (void) setOutputDeviceForPlaythroughAndControlSync:(const BGMAudioDevice&)newOutputDevice { // 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; if (device.DataSourceControlIsSettable(scope, channel)) { DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting dataSourceID=%u", dataSourceID); 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]; [outputDeviceMenuSection outputDeviceDidChange]; } - (NSError*) failedToSetOutputDevice:(AudioDeviceID)deviceID errorCode:(OSStatus)errorCode revertTo:(AudioDeviceID*)revertTo { // Using LogWarning from PublicUtility instead of NSLog here crashes from a bad access. Not sure why. // TODO: Possibly caused by a bug in CADebugMacros.cpp. See commit ab9d4cd. NSLog(@"BGMAudioDeviceManager::failedToSetOutputDevice: Couldn't set device with ID %u as output device. " "%s%d. %@", deviceID, "Error: ", errorCode, (revertTo ? [NSString stringWithFormat:@"Will attempt to revert to the previous device. " "Previous device ID: %u.", *revertTo] : @"")); NSDictionary* __nullable info = nil; if (revertTo) { // Try to reactivate the original device listener and playthrough. (Sorry about the mutual recursion.) NSError* __nullable revertError = [self setOutputDeviceWithID:*revertTo revertOnFailure:NO]; if (revertError) { info = @{ @"revertError": (NSError*)revertError }; } } else { // TODO: Handle this error better in callers. Maybe show an error dialog and try to set the original // default device as the output device. NSLog(@"BGMAudioDeviceManager::failedToSetOutputDevice: Failed to revert to the previous device."); } return [NSError errorWithDomain:@kBGMAppBundleID code:errorCode userInfo:info]; } - (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 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 // stateLock could cause deadlocks. Instead we return early with an error code that BGMDriver knows to // ignore, since the output device is (almost certainly) being changed and we can't avoid dropping frames // while the output device starts up. OSStatus err; BOOL gotLock; @try { gotLock = [stateLock tryLock]; BOOL isBigSur = NO; if (@available(macOS 11.0, *)) { isBigSur = YES; } // Always start playthrough asynchronously. Temp workaround for deadlock on Big Sur. if (!isBigSur && gotLock) { BGMPlayThrough& pt = (forUISoundsDevice ? playThrough_UISounds : playThrough); // 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); BGMLogAndSwallowExceptionsMsg("BGMAudioDeviceManager::startPlayThroughSync", "Starting playthrough (dispatched)", [&] { pt.Start(); }); BGMLogAndSwallowExceptions("BGMAudioDeviceManager::startPlayThroughSync", [&] { pt.StopIfIdle(); }); } @finally { [stateLock unlock]; } }); } } @finally { if (gotLock) { [stateLock unlock]; } } 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 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 ================================================ FILE: BGMApp/BGMApp/BGMAutoPauseMenuItem.h ================================================ // 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 . // // BGMAutoPauseMenuItem.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Local Includes #import "BGMAutoPauseMusic.h" #import "BGMMusicPlayers.h" #import "BGMUserDefaults.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMAutoPauseMenuItem : NSObject - (instancetype) initWithMenuItem:(NSMenuItem*)item autoPauseMusic:(BGMAutoPauseMusic*)autoPause musicPlayers:(BGMMusicPlayers*)players userDefaults:(BGMUserDefaults*)defaults; // Handle events passed along by the delegate (NSMenuDelegate) of the menu containing this menu item. - (void) parentMenuNeedsUpdate; - (void) parentMenuItemWillHighlight:(NSMenuItem* __nullable)item; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAutoPauseMenuItem.m ================================================ // 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 . // // BGMAutoPauseMenuItem.m // BGMApp // // Copyright © 2016, 2019 Kyle Neideck // Copyright © 2016 Tanner Hoke // // Self Include #import "BGMAutoPauseMenuItem.h" // Local Includes #import "BGMAppWatcher.h" #pragma clang assume_nonnull begin static NSString* const kMenuItemTitleFormat = @"Auto-pause %@"; static NSString* const kMenuItemDisabledToolTipFormat = @"%@ doesn't appear to be running."; // Wait time to disable/enable the auto-pause menu item, in seconds. static SInt64 const kMenuItemUpdateWaitTime = 1; @implementation BGMAutoPauseMenuItem { BGMUserDefaults* userDefaults; NSMenuItem* menuItem; BGMAutoPauseMusic* autoPauseMusic; BGMMusicPlayers* musicPlayers; BGMAppWatcher* appWatcher; } - (instancetype) initWithMenuItem:(NSMenuItem*)item autoPauseMusic:(BGMAutoPauseMusic*)autoPause musicPlayers:(BGMMusicPlayers*)players userDefaults:(BGMUserDefaults*)defaults { if ((self = [super init])) { menuItem = item; autoPauseMusic = autoPause; musicPlayers = players; userDefaults = defaults; // Enable/disable auto-pause to match the user's preferences setting. if (userDefaults.autoPauseMusicEnabled) { menuItem.state = NSOnState; [autoPauseMusic enable]; } else { menuItem.state = NSOffState; [autoPauseMusic disable]; } // Toggle auto-pause when the menu item is clicked. menuItem.target = self; menuItem.action = @selector(toggleAutoPauseMusic); [self initMenuItemTitle]; } return self; } - (void) initMenuItemTitle { // Set the initial text, tool-tip, state, etc. [self updateMenuItemTitle]; // Avoid retain cycles in case we ever want to destroy instances of this class. BGMAutoPauseMenuItem* __weak weakSelf = self; // Add a callback that enables/disables the Auto-pause Music menu item when the music player // is launched/terminated. void (^callback)(void) = ^{ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kMenuItemUpdateWaitTime * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ BGMAutoPauseMenuItem* strongSelf = weakSelf; [strongSelf updateMenuItemTitle]; }); }; appWatcher = [[BGMAppWatcher alloc] initWithAppLaunched:callback appTerminated:callback isMatchingBundleID:^BOOL(NSString* appBundleID) { BGMAutoPauseMenuItem* strongSelf = weakSelf; NSString* __nullable playerBundleID = strongSelf->musicPlayers.selectedMusicPlayer.bundleID; return playerBundleID && [appBundleID isEqualToString:(NSString*)playerBundleID]; }]; } - (void) toggleAutoPauseMusic { // The menu item was clicked. if (menuItem.state == NSOnState) { menuItem.state = NSOffState; [autoPauseMusic disable]; } else { menuItem.state = NSOnState; [autoPauseMusic enable]; } // Persist the change in the user's preferences. userDefaults.autoPauseMusicEnabled = (menuItem.state == NSOnState); } - (void) updateMenuItemTitle { [self updateMenuItemTitleWithHighlight:menuItem.isHighlighted]; } - (void) updateMenuItemTitleWithHighlight:(BOOL)highlight { // Set the title of the Auto-pause Music menu item, including the name of the selected music player. NSString* musicPlayerName = musicPlayers.selectedMusicPlayer.name; menuItem.title = [NSString stringWithFormat:kMenuItemTitleFormat, musicPlayerName]; // Make the Auto-pause Music menu item appear disabled if the application is not running. // // We don't actually disable it just in case the user decides to disable auto-pause and their music player isn't // running. E.g. someone who only recently installed Background Music and doesn't want to use auto-pause at all. if (musicPlayers.selectedMusicPlayer.running) { menuItem.attributedTitle = nil; menuItem.toolTip = nil; } else { // Hardcode the text colour grey to match disabled menu items (unless the menu item is highlighted, in which // case use white). // // I couldn't figure out a way to do this without hardcoding the colours. There's no colour constant for this, // except possibly disabledControlTextColor, which just leaves the text black for me. I also couldn't get the // colours from the built-in NSColorLists. // // TODO: Can we make the tick mark grey as well? NSString* __nullable appleInterfaceStyle = [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleInterfaceStyle"]; BOOL darkMode = [appleInterfaceStyle isEqualToString:@"Dark"]; NSColor* textColor = [NSColor colorWithHue:0 saturation:0 brightness:(highlight ? 1 : (darkMode ? 0.25 : 0.75)) alpha:1]; NSDictionary* attributes = @{ NSFontAttributeName: [NSFont menuBarFontOfSize:0], // Default font size NSForegroundColorAttributeName: textColor }; NSAttributedString* pseudoDisabledTitle = [[NSAttributedString alloc] initWithString:menuItem.title attributes:attributes]; menuItem.attributedTitle = pseudoDisabledTitle; menuItem.toolTip = [NSString stringWithFormat:kMenuItemDisabledToolTipFormat, musicPlayerName]; } } #pragma mark Parent menu events - (void) parentMenuNeedsUpdate { [self updateMenuItemTitle]; } - (void) parentMenuItemWillHighlight:(NSMenuItem* __nullable)item { // Used to make the auto-pause menu item's text white when it's highlighted and change it back after. // // TODO: If you click the auto-pause menu item while it's disabled, it will initially appear highlighted next time // you open the main menu. // If item is nil or any other menu item, the auto-pause menu item will be unhighlighted. BOOL willHighlightMenuItem = [item isEqual:menuItem]; // Only update the menu item if it's changing (from highlighted to unhighlighted or vice versa) to save a little // CPU. if (willHighlightMenuItem != menuItem.highlighted) { [self updateMenuItemTitleWithHighlight:willHighlightMenuItem]; } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAutoPauseMusic.h ================================================ // 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 . // // BGMAutoPauseMusic.h // BGMApp // // Copyright © 2016 Kyle Neideck // // When enabled, BGMAutoPauseMusic listens for notifications from BGMDevice to tell when music is playing and // pauses the music player if other audio starts. // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMMusicPlayers.h" #import "BGMUserDefaults.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMAutoPauseMusic : NSObject - (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers userDefaults:(BGMUserDefaults*)inUserDefaults; - (void) enable; - (void) disable; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMAutoPauseMusic.mm ================================================ // 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 . // // BGMAutoPauseMusic.m // BGMApp // // Copyright © 2016, 2017 Kyle Neideck // // Self Include #import "BGMAutoPauseMusic.h" // Local Includes #include "BGM_Types.h" #import "BGMMusicPlayer.h" // STL Includes #import // std::max, std::min // System Includes #include #include // We multiply the time spent paused by this factor to calculate the delay before we consider unpausing. static Float32 const kUnpauseDelayWeightingFactor = 0.1f; @implementation BGMAutoPauseMusic { BOOL enabled; BGMAudioDeviceManager* audioDevices; BGMMusicPlayers* musicPlayers; BGMUserDefaults* userDefaults; dispatch_queue_t listenerQueue; // Have to keep track of the listener block we add so we can remove it later. AudioObjectPropertyListenerBlock listenerBlock; dispatch_queue_t pauseUnpauseMusicQueue; // True if BGMApp has paused musicPlayer and hasn't unpaused it yet. (Will be out of sync with the music player app if the // user has unpaused it themselves.) BOOL wePaused; // The times, in absolute time, that the BGMDevice last changed its audible state to silent... UInt64 wentSilent; // ...and to audible. UInt64 wentAudible; } - (id) initWithAudioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers userDefaults:(BGMUserDefaults*)inUserDefaults { if ((self = [super init])) { audioDevices = inAudioDevices; musicPlayers = inMusicPlayers; userDefaults = inUserDefaults; enabled = NO; wePaused = NO; dispatch_queue_attr_t attr; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" if (&dispatch_queue_attr_make_with_qos_class) { attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0); } else { // OS X 10.9 fallback attr = DISPATCH_QUEUE_SERIAL; } #pragma clang diagnostic pop listenerQueue = dispatch_queue_create("com.bearisdriving.BGM.AutoPauseMusic.Listener", attr); pauseUnpauseMusicQueue = dispatch_queue_create("com.bearisdriving.BGM.AutoPauseMusic.PauseUnpauseMusic", attr); [self initListenerBlock]; } return self; } - (void) dealloc { [self disable]; } - (void) initListenerBlock { // To avoid retain cycle __unsafe_unretained BGMAutoPauseMusic* weakSelf = self; listenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress * _Nonnull inAddresses) { // inAddresses "may contain addresses for properties for which the listener is not signed up to receive notifications", // so we have to check them all for (int i = 0; i < inNumberAddresses; i++) { if (inAddresses[i].mSelector == kAudioDeviceCustomPropertyDeviceAudibleState) { BGMDeviceAudibleState audibleState = [weakSelf deviceAudibleState]; #if DEBUG const char audibleStateStr[5] = CA4CCToCString(audibleState); DebugMsg("BGMAutoPauseMusic::initListenerBlock: kAudioDeviceCustomPropertyDeviceAudibleState property changed to '%s'", audibleStateStr); #endif // TODO: We shouldn't assume this block will only get called when BGMDevice's audible state changes. (Even if // the Core Audio docs did specify that, there's no reason not to be fault tolerant.) if (audibleState == kBGMDeviceIsAudible) { [weakSelf queuePauseBlock]; } else if (audibleState == kBGMDeviceIsSilent) { [weakSelf queueUnpauseBlock]; } else if (audibleState == kBGMDeviceIsSilentExceptMusic) { // If we pause the music player and then the user unpauses it before the other audio stops, we need to set // wePaused to false at some point before the other audio starts again so we know we should pause DebugMsg("BGMAutoPauseMusic: Device is silent except music, resetting wePaused flag"); wePaused = NO; } // TODO: Add a fourth audible state, something like "AudibleAndMusicPlaying", and check it here to // handle the user unpausing and then repausing music while also playing other audio? } } }; } - (BGMDeviceAudibleState) deviceAudibleState { return [audioDevices bgmDevice].GetAudibleState(); } - (void) queuePauseBlock { UInt64 now = mach_absolute_time(); wentAudible = now; UInt64 startedPauseDelay = now; UInt64 pauseDelayMS = userDefaults.pauseDelayMS; // If pause delay is 0, pause immediately (no delay) if (pauseDelayMS == 0) { DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Pause delay is 0, pausing immediately"); // Pause immediately if device is audible and we haven't already paused if (!wePaused && ([self deviceAudibleState] == kBGMDeviceIsAudible)) { wePaused = ([musicPlayers.selectedMusicPlayer pause] || wePaused); } return; } UInt64 pauseDelayNSec = pauseDelayMS * NSEC_PER_MSEC; DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Dispatching pause block at %llu", now); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, pauseDelayNSec), pauseUnpauseMusicQueue, ^{ BOOL stillAudible = ([self deviceAudibleState] == kBGMDeviceIsAudible); DebugMsg("BGMAutoPauseMusic::queuePauseBlock: Running pause block dispatched at %llu.%s wentAudible=%llu", startedPauseDelay, stillAudible ? "" : " Not pausing because the device isn't audible.", wentAudible); // Pause if this is the most recent pause block and the device is still audible, which means the audible // state hasn't changed since this block was queued. Also set wePaused to true if the player wasn't // already paused. if (!wePaused && (startedPauseDelay == wentAudible) && stillAudible) { wePaused = ([musicPlayers.selectedMusicPlayer pause] || wePaused); } }); } - (void) queueUnpauseBlock { UInt64 now = mach_absolute_time(); wentSilent = now; UInt64 startedUnpauseDelay = now; // Get user-configurable max delay UInt64 maxUnpauseDelayMS = userDefaults.maxUnpauseDelayMS; // If max unpause delay is 0, unpause immediately (no delay) if (maxUnpauseDelayMS == 0) { DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Max unpause delay is 0, unpausing immediately"); // Unpause immediately if we were the one who paused and device is still silent BGMDeviceAudibleState currentState = [self deviceAudibleState]; DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Immediate unpause - wePaused=%s, currentState=%s", wePaused ? "YES" : "NO", currentState == kBGMDeviceIsSilent ? "Silent" : (currentState == kBGMDeviceIsAudible ? "Audible" : "SilentExceptMusic")); if (wePaused && (currentState == kBGMDeviceIsSilent)) { wePaused = NO; [musicPlayers.selectedMusicPlayer unpause]; } return; } // 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: 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((wentSilent - wentAudible) * kUnpauseDelayWeightingFactor); // Convert from absolute time to nanos. mach_timebase_info_data_t info; mach_timebase_info(&info); unpauseDelayNsec = unpauseDelayNsec * info.numer / info.denom; // Clamp using user-configurable max delay and calculated min delay. UInt64 maxUnpauseDelayNSec = maxUnpauseDelayMS * NSEC_PER_MSEC; UInt64 minUnpauseDelayNSec = maxUnpauseDelayNSec / 10; unpauseDelayNsec = std::min(maxUnpauseDelayNSec, unpauseDelayNsec); unpauseDelayNsec = std::max(minUnpauseDelayNSec, unpauseDelayNsec); DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Dispatched unpause block at %llu. unpauseDelayNsec=%llu", now, unpauseDelayNsec); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, unpauseDelayNsec), pauseUnpauseMusicQueue, ^{ BGMDeviceAudibleState currentState = [self deviceAudibleState]; BOOL stillSilent = (currentState == kBGMDeviceIsSilent); BOOL isLatestUnpause = (startedUnpauseDelay == wentSilent); DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Running unpause block dispatched at %llu. wePaused=%s, isLatest=%s, currentState=%s, wentSilent=%llu", startedUnpauseDelay, wePaused ? "YES" : "NO", isLatestUnpause ? "YES" : "NO", currentState == kBGMDeviceIsSilent ? "Silent" : (currentState == kBGMDeviceIsAudible ? "Audible" : "SilentExceptMusic"), wentSilent); // Unpause if we were the one who paused. Also check that this is the most recent unpause block and the // device is still silent, which means the audible state hasn't changed since this block was queued. if (wePaused && isLatestUnpause && stillSilent) { DebugMsg("BGMAutoPauseMusic::queueUnpauseBlock: Unpausing music player"); wePaused = NO; [musicPlayers.selectedMusicPlayer unpause]; } }); } - (void) enable { if (!enabled) { [audioDevices bgmDevice].AddPropertyListenerBlock(kBGMAudibleStateAddress, listenerQueue, listenerBlock); enabled = YES; } } - (void) disable { if (enabled) { [audioDevices bgmDevice].RemovePropertyListenerBlock(kBGMAudibleStateAddress, listenerQueue, listenerBlock); enabled = NO; } } @end ================================================ FILE: BGMApp/BGMApp/BGMBackgroundMusicDevice.cpp ================================================ // 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 . // // BGMBackgroundMusicDevice.cpp // BGMApp // // Copyright © 2016-2019 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 #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(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 BGMBackgroundMusicDevice::ResponsibleBundleIDsOf(CACFString inParentBundleID) { if(!inParentBundleID.IsValid()) { return {}; } std::map> 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", "com.hnc.Discord.helper.Renderer", "com.hnc.Discord.helper.Plugin" } }, // Brave { "com.brave.Browser", { "com.brave.Browser.helper", "com.brave.Browser.helper.plugin" } }, // Skype { "com.skype.skype", { "com.skype.skype.Helper" } }, // Google Chrome { "com.google.Chrome", { "com.google.Chrome.helper" } }, // Microsoft Edge { "com.microsoft.edgemac", { "com.microsoft.edgemac.helper" } }, // Arc { "company.thebrowser.Browser", { "company.thebrowser.browser.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(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(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 ================================================ FILE: BGMApp/BGMApp/BGMBackgroundMusicDevice.h ================================================ // 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 . // // 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 #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 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 */ ================================================ FILE: BGMApp/BGMApp/BGMDebugLoggingMenuItem.h ================================================ // 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 . // // BGMDebugLoggingMenuItem.h // BGMApp // // Copyright © 2020 Kyle Neideck // // A menu item in the main menu that enables/disables debug logging. Only visible if you hold the // option down when you click the status bar icon to reveal the main menu. // // TODO: It would be better to have this menu item in the Preferences menu (maybe in an Advanced // section) and always visible, but first we'd need to add something that tells the user how // to view the log messages. Or better yet, something that automatically opens them. // // System Includes #import #pragma clang assume_nonnull begin @interface BGMDebugLoggingMenuItem : NSObject - (instancetype) initWithMenuItem:(NSMenuItem*)menuItem; // True if the main menu is showing hidden items/options because the user held the option key when // they clicked the icon. This class makes the debug logging menu item visible if this property has // been set true or if debug logging is enabled. @property (nonatomic) BOOL menuShowingExtraOptions; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMDebugLoggingMenuItem.m ================================================ // 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 . // // BGMDebugLoggingMenuItem.m // BGMApp // // Copyright © 2020 Kyle Neideck // // Self Include #import "BGMDebugLoggingMenuItem.h" // PublicUtility Includes #import "BGMDebugLogging.h" #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMDebugLoggingMenuItem { NSMenuItem* _menuItem; BOOL _menuShowingExtraOptions; } - (instancetype) initWithMenuItem:(NSMenuItem*)menuItem { if ((self = [super init])) { _menuItem = menuItem; _menuItem.state = BGMDebugLoggingIsEnabled() ? NSControlStateValueOn : NSControlStateValueOff; [self setMenuShowingExtraOptions:NO]; // Enable/disable debug logging when the menu item is clicked. menuItem.target = self; menuItem.action = @selector(toggleDebugLogging); } return self; } - (void) setMenuShowingExtraOptions:(BOOL)showingExtra { _menuShowingExtraOptions = showingExtra; _menuItem.hidden = !BGMDebugLoggingIsEnabled() && !showingExtra; DebugMsg("BGMDebugLoggingMenuItem::menuShowingExtraOptions: %s the menu item", _menuItem.hidden ? "Hiding" : "Showing"); } - (void) toggleDebugLogging { BGMSetDebugLoggingEnabled(!BGMDebugLoggingIsEnabled()); _menuItem.state = BGMDebugLoggingIsEnabled() ? NSControlStateValueOn : NSControlStateValueOff; DebugMsg("BGMDebugLoggingMenuItem::toggleDebugLogging: Debug logging %s", BGMDebugLoggingIsEnabled() ? "enabled" : "disabled"); } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMDeviceControlSync.cpp ================================================ // 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 . // // BGMDeviceControlSync.cpp // BGMApp // // Copyright © 2016, 2017 Kyle Neideck // // Self Include #include "BGMDeviceControlSync.h" // Local Includes #include "BGM_Types.h" #include "BGM_Utils.h" // PublicUtility Includes #include "CAPropertyAddress.h" #pragma clang assume_nonnull begin static const AudioObjectPropertyAddress kMutePropertyAddress = { kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster }; static const AudioObjectPropertyAddress kVolumePropertyAddress = { kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster }; #pragma mark Construction/Destruction BGMDeviceControlSync::BGMDeviceControlSync(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice, CAHALAudioSystemObject inAudioSystem) : mBGMDevice(inBGMDevice), mOutputDevice(inOutputDevice), mAudioSystem(inAudioSystem), mBGMDeviceControlsList(inBGMDevice) { } BGMDeviceControlSync::~BGMDeviceControlSync() { BGMLogAndSwallowExceptions("BGMDeviceControlSync::~BGMDeviceControlSync", [&] { CAMutex::Locker locker(mMutex); Deactivate(); }); } void BGMDeviceControlSync::Activate() { CAMutex::Locker locker(mMutex); ThrowIf((mBGMDevice.GetObjectID() == kAudioObjectUnknown || mOutputDevice.GetObjectID() == kAudioObjectUnknown), BGM_DeviceNotSetException(), "BGMDeviceControlSync::Activate: Both the output device and BGMDevice must be set to start synchronizing their controls"); if(!mActive) { DebugMsg("BGMDeviceControlSync::Activate: Activating control sync"); // Disable BGMDevice controls that the output device doesn't have and reenable any that were // disabled for the previous output device. // // Continue anyway if this fails because it's better to have extra/missing controls than to // be unable to use the device. BGMLogAndSwallowExceptionsMsg("BGMDeviceControlSync::Activate", "Controls list", [&] { bool wasUpdated = mBGMDeviceControlsList.MatchControlsListOf(mOutputDevice); if(wasUpdated) { mBGMDeviceControlsList.PropagateControlListChange(); } }); // Init BGMDevice controls to match output device mBGMDevice.CopyVolumeFrom(mOutputDevice, kAudioObjectPropertyScopeOutput); mBGMDevice.CopyMuteFrom(mOutputDevice, kAudioObjectPropertyScopeOutput); // Register listeners for volume and mute values mBGMDevice.AddPropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this); try { mBGMDevice.AddPropertyListener(kMutePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this); } catch(CAException) { CATry mBGMDevice.RemovePropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this); CACatch throw; } mActive = true; } else { DebugMsg("BGMDeviceControlSync::Activate: Already active"); } } void BGMDeviceControlSync::Deactivate() { CAMutex::Locker locker(mMutex); if(mActive) { DebugMsg("BGMDeviceControlSync::Deactivate: Deactivating control sync"); // Deregister listeners if(mBGMDevice.GetObjectID() != kAudioDeviceUnknown) { BGMLogAndSwallowExceptions("BGMDeviceControlSync::Deactivate", [&] { mBGMDevice.RemovePropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this); }); BGMLogAndSwallowExceptions("BGMDeviceControlSync::Deactivate", [&] { mBGMDevice.RemovePropertyListener(kMutePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this); }); } mActive = false; } else { DebugMsg("BGMDeviceControlSync::Deactivate: Not active"); } } #pragma mark Accessors void BGMDeviceControlSync::SetDevices(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice) { CAMutex::Locker locker(mMutex); bool wasActive = mActive; Deactivate(); mBGMDevice = inBGMDevice; mBGMDeviceControlsList.SetBGMDevice(inBGMDevice); mOutputDevice = inOutputDevice; if(wasActive) { Activate(); } } #pragma mark Listener Procs // static OSStatus BGMDeviceControlSync::BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData) { // refCon (reference context) is the instance that registered this listener proc. BGMDeviceControlSync* refCon = static_cast(inClientData); auto checkState = [&] { if(!refCon) { LogError("BGMDeviceControlSync::BGMDeviceListenerProc: !refCon"); return false; } if(!refCon->mActive || (refCon->mBGMDevice.GetObjectID() == kAudioObjectUnknown) || (refCon->mOutputDevice.GetObjectID() == kAudioObjectUnknown)) { return false; } if(inObjectID != refCon->mBGMDevice.GetObjectID()) { LogError("BGMDeviceControlSync::BGMDeviceListenerProc: notified about audio object other than BGMDevice"); return false; } return true; }; for(int i = 0; i < inNumberAddresses; i++) { AudioObjectPropertyScope scope = inAddresses[i].mScope; switch(inAddresses[i].mSelector) { case kAudioDevicePropertyVolumeScalar: { CAMutex::Locker locker(refCon->mMutex); // Update the output device's volume. if(checkState()) { refCon->mOutputDevice.CopyVolumeFrom(refCon->mBGMDevice, scope); } } break; case kAudioDevicePropertyMute: { CAMutex::Locker locker(refCon->mMutex); // Update the output device's mute control. Note that this also runs when you // change the volume (on BGMDevice). if(checkState()) { refCon->mOutputDevice.CopyMuteFrom(refCon->mBGMDevice, scope); } } break; } } // "The return value [of an AudioObjectPropertyListenerProc] is currently unused and should always be 0." return 0; } #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMDeviceControlSync.h ================================================ // 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 . // // BGMDeviceControlSync.h // BGMApp // // Copyright © 2016, 2017 Kyle Neideck // // Synchronises BGMDevice's controls (just volume and mute currently) with the output device's // controls. This allows the user to control the output device normally while BGMDevice is set as // the default device. // // BGMDeviceControlSync disables any BGMDevice controls that the output device doesn't also have. // When the value of one of BGMDevice's controls is changed, BGMDeviceControlSync copies the new // value to the output device. // // Thread safe. // #ifndef BGMApp__BGMDeviceControlSync #define BGMApp__BGMDeviceControlSync // Local Includes #include "BGMAudioDevice.h" #include "BGMDeviceControlsList.h" // PublicUtility Includes #include "CAHALAudioSystemObject.h" #include "CAMutex.h" // System Includes #include #pragma clang assume_nonnull begin class BGMDeviceControlSync { #pragma mark Construction/Destruction public: BGMDeviceControlSync(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice, CAHALAudioSystemObject inAudioSystem = CAHALAudioSystemObject()); ~BGMDeviceControlSync(); // Disallow copying BGMDeviceControlSync(const BGMDeviceControlSync&) = delete; BGMDeviceControlSync& operator=(const BGMDeviceControlSync&) = delete; #ifdef __OBJC__ // Only intended as a convenience for Objective-C instance vars BGMDeviceControlSync() : BGMDeviceControlSync(kAudioObjectUnknown, kAudioObjectUnknown) { }; #endif /*! Begin synchronising BGMDevice's controls with the output device's. @throws BGM_DeviceNotSetException if BGMDevice isn't set. @throws CAException if the HAL or one of the devices returns an error when this function registers for device property notifications or when it copies the current values of the output device's controls to BGMDevice. This BGMDeviceControlSync will remain inactive if this function throws. */ void Activate(); /*! Stop synchronising BGMDevice's controls with the output device's. */ void Deactivate(); #pragma mark Accessors /*! Set the IDs of BGMDevice and the output device to synchronise with. @throws BGM_DeviceNotSetException if BGMDevice isn't set. @throws CAException if the HAL or one of the new devices returns an error while restarting synchronisation. This BGMDeviceControlSync will be deactivated if this function throws, but its devices will still be set. */ void SetDevices(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice); #pragma mark Listener Procs private: /*! Receives HAL notifications about the BGMDevice properties this class listens to. */ static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData); private: CAMutex mMutex { "Device Control Sync" }; bool mActive = false; CAHALAudioSystemObject mAudioSystem; BGMAudioDevice mBGMDevice { (AudioObjectID)kAudioObjectUnknown }; BGMAudioDevice mOutputDevice { (AudioObjectID)kAudioObjectUnknown }; BGMDeviceControlsList mBGMDeviceControlsList; }; #pragma clang assume_nonnull end #endif /* BGMApp__BGMDeviceControlSync */ ================================================ FILE: BGMApp/BGMApp/BGMDeviceControlsList.cpp ================================================ // 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 . // // BGMDeviceControlsList.cpp // BGMApp // // Copyright © 2017 Kyle Neideck // // Self Include #include "BGMDeviceControlsList.h" // Local Includes #include "BGM_Types.h" #include "BGM_Utils.h" // PublicUtility Includes #include "CAPropertyAddress.h" #include "CACFArray.h" #pragma clang assume_nonnull begin static const SInt64 kToggleDeviceInitialDelay = 50 * NSEC_PER_MSEC; static const SInt64 kToggleDeviceBackDelay = 500 * NSEC_PER_MSEC; static const SInt64 kDisableNullDeviceDelay = 500 * NSEC_PER_MSEC; static const SInt64 kDisableNullDeviceTimeout = 5000 * NSEC_PER_MSEC; #pragma mark Construction/Destruction BGMDeviceControlsList::BGMDeviceControlsList(AudioObjectID inBGMDevice, CAHALAudioSystemObject inAudioSystem) : mBGMDevice(inBGMDevice), mAudioSystem(inAudioSystem) { BGMAssert((mBGMDevice.IsBGMDevice() || mBGMDevice.GetObjectID() == kAudioObjectUnknown), "BGMDeviceControlsList::BGMDeviceControlsList: Given device is not BGMDevice"); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" mCanToggleDeviceOnSystem = (&dispatch_block_wait && &dispatch_block_cancel && &dispatch_block_testcancel && &dispatch_queue_attr_make_with_qos_class); #pragma clang diagnostic pop } BGMDeviceControlsList::~BGMDeviceControlsList() { CAMutex::Locker locker(mMutex); if(!mDeviceTogglingInitialised) { return; } 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) { DebugMsg("BGMDeviceControlsList::~BGMDeviceControlsList: Waiting for device toggle"); // Copy the reference so we can unlock the mutex and allow any remaining blocks to run. dispatch_block_t disableNullDeviceBlock = mDisableNullDeviceBlock; CAMutex::Unlocker unlocker(mMutex); // Note that if mDisableNullDeviceBlock is currently running this will return after it // finishes and if it's already run this will return immediately. So we don't have to // worry about ending up waiting for mDisableNullDeviceBlock when it isn't queued. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" long timedOut = dispatch_block_wait(disableNullDeviceBlock, kDisableNullDeviceTimeout); #pragma clang diagnostic pop if(timedOut) { LogWarning("BGMDeviceControlsList::~BGMDeviceControlsList: Device toggle timed out"); } } mDeviceToggleState = ToggleState::NotToggling; DestroyBlock(mDeviceToggleBlock); DestroyBlock(mDeviceToggleBackBlock); DestroyBlock(mDisableNullDeviceBlock); if(mListenerBlock) { Block_release(mListenerBlock); } if(mListenerQueue) { dispatch_release(BGM_Utils::NN(mListenerQueue)); } } #pragma mark Accessors void BGMDeviceControlsList::SetBGMDevice(AudioObjectID inBGMDeviceID) { CAMutex::Locker locker(mMutex); mBGMDevice = inBGMDeviceID; BGMAssert(mBGMDevice.IsBGMDevice(), "BGMDeviceControlsList::SetBGMDevice: Given device is not BGMDevice"); } #pragma mark Update Controls List bool BGMDeviceControlsList::MatchControlsListOf(AudioObjectID inDeviceID) { CAMutex::Locker locker(mMutex); if(!mBGMDevice.IsBGMDevice()) { LogWarning("BGMDeviceControlsList::MatchControlsListOf: BGMDevice ID not set"); return false; } // If the output device doesn't have a control that BGMDevice does, disable it on BGMDevice so // the system's audio UI isn't confusing. // No need to change input controls. AudioObjectPropertyScope inScope = kAudioObjectPropertyScopeOutput; // Check which of BGMDevice's controls are currently enabled. We need to know whether we're // actually enabling/disabling any controls so we know whether we need to call // PropagateControlListChange afterward. CFTypeRef __nullable enabledControlsRef = mBGMDevice.GetPropertyData_CFType(kBGMEnabledOutputControlsAddress); ThrowIf(!enabledControlsRef || (CFGetTypeID(enabledControlsRef) != CFArrayGetTypeID()), CAException(kAudioHardwareIllegalOperationError), "BGMDeviceControlsList::MatchControlsListOf: Expected a CFArray for " "kAudioDeviceCustomPropertyEnabledOutputControls"); CACFArray enabledControls(static_cast(enabledControlsRef), true); BGMAssert(enabledControls.GetNumberItems() == 2, "BGMDeviceControlsList::MatchControlsListOf: Expected 2 array elements for " "kAudioDeviceCustomPropertyEnabledOutputControls"); bool volumeEnabled; bool didGetBool = enabledControls.GetBool(kBGMEnabledOutputControlsIndex_Volume, volumeEnabled); ThrowIf(!didGetBool, CAException(kAudioHardwareIllegalOperationError), "BGMDeviceControlsList::MatchControlsListOf: Expected volume element of " "kAudioDeviceCustomPropertyEnabledOutputControls to be a CFBoolean"); bool muteEnabled; didGetBool = enabledControls.GetBool(kBGMEnabledOutputControlsIndex_Mute, muteEnabled); ThrowIf(!didGetBool, CAException(kAudioHardwareIllegalOperationError), "BGMDeviceControlsList::MatchControlsListOf: Expected mute element of " "kAudioDeviceCustomPropertyEnabledOutputControls to be a CFBoolean"); DebugMsg("BGMDeviceControlsList::MatchControlsListOf: BGMDevice has volume %s, mute %s", (volumeEnabled ? "enabled" : "disabled"), (muteEnabled ? "enabled" : "disabled")); // Check which controls the other device has. BGMAudioDevice device(inDeviceID); bool hasMute = device.HasSettableMasterMute(inScope); bool hasVolume = device.HasSettableMasterVolume(inScope) || device.HasSettableVirtualMasterVolume(inScope); if(!hasVolume) { // Check for per-channel volume controls. UInt32 numChannels = device.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput); for(UInt32 channel = 1; channel <= numChannels; channel++) { BGMLogAndSwallowExceptionsMsg("BGMDeviceControlsList::MatchControlsListOf", "Checking for channel volume controls", ([&] { hasVolume = (device.HasVolumeControl(inScope, channel) && device.VolumeControlIsSettable(inScope, channel)); })); if(hasVolume) { break; } } } // Tell BGMDevice to enable/disable its controls to match the output device. bool deviceUpdated = false; CACFArray newEnabledControls; newEnabledControls.SetCFMutableArrayFromCopy(enabledControls.GetCFArray()); // Update volume. if(volumeEnabled != hasVolume) { DebugMsg("BGMDeviceControlsList::MatchControlsListOf: %s BGMDevice volume control.", hasVolume ? "Enabling" : "Disabling"); newEnabledControls.SetBool(kBGMEnabledOutputControlsIndex_Volume, hasVolume); deviceUpdated = true; } // Update mute. if(muteEnabled != hasMute) { DebugMsg("BGMDeviceControlsList::MatchControlsListOf: %s BGMDevice mute control.", hasMute ? "Enabling" : "Disabling"); newEnabledControls.SetBool(kBGMEnabledOutputControlsIndex_Mute, hasMute); deviceUpdated = true; } if(deviceUpdated) { mBGMDevice.SetPropertyData_CFType(kBGMEnabledOutputControlsAddress, newEnabledControls.GetCFMutableArray()); } return deviceUpdated; } void BGMDeviceControlsList::PropagateControlListChange() { CAMutex::Locker locker(mMutex); if((mBGMDevice == kAudioObjectUnknown) || !mCanToggleDeviceOnSystem) { return; } InitDeviceToggling(); // Leave the default device alone if the user has changed it since launching BGMApp. bool bgmDeviceIsDefault = true; BGMLogAndSwallowExceptions("BGMDeviceControlsList::PropagateControlListChange", ([&] { bgmDeviceIsDefault = (mBGMDevice.GetObjectID() == mAudioSystem.GetDefaultAudioDevice(false, false)); })); if(bgmDeviceIsDefault) { mDeviceToggleState = ToggleState::SettingNullDeviceAsDefault; // We'll get a notification from the HAL after the Null Device is enabled. Then we can // temporarily make it the default device, which gets other programs to notice that // BGMDevice's controls have changed. try { CAMutex::Unlocker unlocker(mMutex); SetNullDeviceEnabled(true); } catch (...) { mDeviceToggleState = ToggleState::NotToggling; LogError("BGMDeviceControlsList::PropagateControlListChange: Could not enable the Null " "Device"); throw; } } } #pragma mark Implementation void BGMDeviceControlsList::InitDeviceToggling() { CAMutex::Locker locker(mMutex); if(mDeviceTogglingInitialised || !mCanToggleDeviceOnSystem) { return; } BGMAssert(mBGMDevice.IsBGMDevice(), "BGMDeviceControlsList::InitDeviceToggling: mBGMDevice device is not set to " "BGMDevice's ID"); // Register a listener to find out when the Null Device becomes available/unavailable. See // ToggleDefaultDevice. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0); #pragma clang diagnostic pop mListenerQueue = dispatch_queue_create("com.bearisdriving.BGM.BGMDeviceControlsList", attr); 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) { return; } for(int i = 0; i < inNumberAddresses; i++) { switch(inAddresses[i].mSelector) { case kAudioHardwarePropertyDevices: { CAMutex::Locker innerLocker(mMutex); DebugMsg("BGMDeviceControlsList::InitDeviceToggling: Got " "kAudioHardwarePropertyDevices"); // Cancel the previous block in case it hasn't run yet. DestroyBlock(mDeviceToggleBlock); mDeviceToggleBlock = CreateDeviceToggleBlock(); // Changing the default device too quickly after enabling the Null Device // seems to cause problems with some programs. Not sure why. #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, mListenerBlock); }); mDeviceTogglingInitialised = true; } void BGMDeviceControlsList::ToggleDefaultDevice() { // Set the Null Device as the OS X default device. AudioObjectID nullDeviceID = mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMNullDeviceUID)); if(nullDeviceID == kAudioObjectUnknown) { // It's unlikely, but we might have been notified about an unrelated device so just log a // warning. LogWarning("BGMDeviceControlsList::ToggleDefaultDevice: Null Device not found"); return; } DebugMsg("BGMDeviceControlsList::ToggleDefaultDevice: Setting Null Device as default. " "nullDeviceID = %u", nullDeviceID); mAudioSystem.SetDefaultAudioDevice(false, false, nullDeviceID); mDeviceToggleState = ToggleState::SettingBGMDeviceAsDefault; // A small number of apps (e.g. Firefox) seem to have trouble with the default device being // changed back immediately, so for now we insert a short delay here and before disabling the // Null Device. // Cancel the previous block in case it hasn't run yet. DestroyBlock(mDeviceToggleBackBlock); mDeviceToggleBackBlock = CreateDeviceToggleBackBlock(); #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) { DebugMsg("BGMDeviceControlsList::SetNullDeviceEnabled: %s the null device", inEnabled ? "Enabling" : "Disabling"); // Get the audio object for BGMDriver, which is the object the Null Device belongs to. AudioObjectID bgmDriverID = mAudioSystem.GetAudioPlugInForBundleID(CFSTR(kBGMDriverBundleID)); if(bgmDriverID == kAudioObjectUnknown) { LogError("BGMDeviceControlsList::SetNullDeviceEnabled: BGMDriver plug-in audio object not " "found"); throw CAException(kAudioHardwareUnspecifiedError); } CAHALAudioObject bgmDriver(bgmDriverID); bgmDriver.SetPropertyData_CFType(CAPropertyAddress(kAudioPlugInCustomPropertyNullDeviceActive), (inEnabled ? kCFBooleanTrue : kCFBooleanFalse)); } dispatch_block_t __nullable BGMDeviceControlsList::CreateDeviceToggleBlock() { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" 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) { BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDeviceToggleBlock", ([&] { ToggleDefaultDevice(); })); } }); if(!toggleBlock) { // Pretty sure this should never happen, but the docs aren't completely clear. LogError("BGMDeviceControlsList::CreateDeviceToggleBlock: !toggleBlock"); } return toggleBlock; } dispatch_block_t __nullable BGMDeviceControlsList::CreateDeviceToggleBackBlock() { #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) { return; } // Set BGMDevice back as the default device. DebugMsg("BGMDeviceControlsList::ToggleDefaultDevice: Setting BGMDevice as default"); BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDeviceToggleBackBlock", ([&] { mAudioSystem.SetDefaultAudioDevice(false, false, mBGMDevice.GetObjectID()); })); mDeviceToggleState = ToggleState::DisablingNullDevice; // Cancel the previous block in case it hasn't run yet. DestroyBlock(mDisableNullDeviceBlock); mDisableNullDeviceBlock = CreateDisableNullDeviceBlock(); #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 __nullable BGMDeviceControlsList::CreateDisableNullDeviceBlock() { #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) { return; } mDeviceToggleState = ToggleState::NotToggling; BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDisableNullDeviceBlock", ([&] { CAMutex::Unlocker unlocker(mMutex); // Hide the null device from the user again. SetNullDeviceEnabled(false); })); 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) { if(!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)) { // Stop the block from running if it's currently queued. dispatch_block_cancel(blockNN); // Make sure the block isn't currently running. That should almost never be the case. while(!dispatch_block_testcancel(blockNN)) { CAMutex::Unlocker unlocker(mMutex); usleep(10); } Block_release(block); block = nullptr; } #pragma clang diagnostic pop } #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMDeviceControlsList.h ================================================ // 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 . // // BGMDeviceControlsList.h // BGMApp // // Copyright © 2017 Kyle Neideck // #ifndef BGMApp__BGMDeviceControlsList #define BGMApp__BGMDeviceControlsList // Local Includes #include "BGMAudioDevice.h" // PublicUtility Includes #include "CAHALAudioDevice.h" #include "CAHALAudioSystemObject.h" #include "CAMutex.h" // System Includes #include #include #pragma clang assume_nonnull begin class BGMDeviceControlsList { #pragma mark Construction/Destruction public: BGMDeviceControlsList(AudioObjectID inBGMDevice, CAHALAudioSystemObject inAudioSystem = CAHALAudioSystemObject()); ~BGMDeviceControlsList(); // Disallow copying BGMDeviceControlsList(const BGMDeviceControlsList&) = delete; BGMDeviceControlsList& operator=(const BGMDeviceControlsList&) = delete; #pragma mark Accessors /*! @param inBGMDeviceID The ID of BGMDevice. */ void SetBGMDevice(AudioObjectID inBGMDeviceID); #pragma mark Update Controls List /*! Enable the BGMDevice controls (volume and mute currently) that can be matched to controls of the given device, and disable the ones that can't. @param inDeviceID The ID of the device. @return True if BGMDevice's list of controls was updated. @throws CAException if an error is received from either device. */ bool MatchControlsListOf(AudioObjectID inDeviceID); /*! After updating BGMDevice's controls list, we need to change the default device so programs (including OS X's audio UI) will update themselves. We could just change to the real output device and change back, but that could have side effects the user wouldn't expect. For example, an app the user has muted might be unmuted for a short period. Instead we tell BGMDriver to enable the Null Device -- a device that does nothing -- so we can use it to toggle the default device. The Null Device is normally disabled so it can be hidden from the user. OS X won't let us make a hidden device temporarily visible or set a hidden device as the default, so we have to completely remove the Null Device from the system while we're not using it. @throws CAException if it fails to enable the Null Device. */ void PropagateControlListChange(); #pragma mark Implementation private: /*! Lazily initialises the fields used to toggle the default device. */ void InitDeviceToggling(); /*! Changes the OS X default audio device to the Null Device and then back to BGMDevice. */ void ToggleDefaultDevice(); /*! Enable or disable the Null Device. See PropagateControlListChange and BGM_NullDevice in BGMDriver. @throws CAException if we can't get the BGMDriver plug-in audio object from the HAL or the HAL returns an error when setting kAudioPlugInCustomPropertyNullDeviceActive. */ void SetNullDeviceEnabled(bool inEnabled); dispatch_block_t __nullable CreateDeviceToggleBlock(); dispatch_block_t __nullable CreateDeviceToggleBackBlock(); dispatch_block_t __nullable CreateDisableNullDeviceBlock(); void DestroyBlock(dispatch_block_t __nullable & block); private: CAMutex mMutex { "Device Controls List" }; bool mDeviceTogglingInitialised = false; // OS X 10.9 doesn't have the functions we use for PropagateControlListChange. bool mCanToggleDeviceOnSystem; BGMAudioDevice mBGMDevice; CAHALAudioSystemObject mAudioSystem; // Not guarded by the mutex. enum ToggleState { NotToggling, SettingNullDeviceAsDefault, SettingBGMDeviceAsDefault, DisablingNullDevice }; BGMDeviceControlsList::ToggleState mDeviceToggleState = ToggleState::NotToggling; dispatch_block_t __nullable mDeviceToggleBlock = nullptr; dispatch_block_t __nullable mDeviceToggleBackBlock = nullptr; dispatch_block_t __nullable mDisableNullDeviceBlock = nullptr; // 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; }; #pragma clang assume_nonnull end #endif /* BGMApp__BGMDeviceControlsList */ ================================================ FILE: BGMApp/BGMApp/BGMOutputDeviceMenuSection.h ================================================ // 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 . // // BGMOutputDeviceMenuSection.h // BGMApp // // Copyright © 2016, 2018 Kyle Neideck // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMPreferredOutputDevices.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMOutputDeviceMenuSection : NSObject - (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices; // To be called when BGMApp has been set to use a different output device. For example, when a new // device is connected and BGMPreferredOutputDevices decides BGMApp should switch to it. - (void) outputDeviceDidChange; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMOutputDeviceMenuSection.mm ================================================ // 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 . // // BGMOutputDeviceMenuSection.mm // BGMApp // // Copyright © 2016-2018 Kyle Neideck // // Self Include #import "BGMOutputDeviceMenuSection.h" // Local Includes #import "BGM_Utils.h" #import "BGM_Types.h" #import "BGMAudioDevice.h" // PublicUtility Includes #import "CAAutoDisposer.h" #import "CAHALAudioDevice.h" #import "CAHALAudioSystemObject.h" #import "CAPropertyAddress.h" // STL Includes #import #import #pragma clang assume_nonnull begin static NSInteger const kOutputDeviceMenuItemTag = 5; @implementation BGMOutputDeviceMenuSection { NSMenu* bgmMenu; BGMAudioDeviceManager* audioDevices; BGMPreferredOutputDevices* preferredDevices; NSMutableArray* outputDeviceMenuItems; // Called when a CoreAudio property has changed and we might need to update the menu. For // example, when a device is connected or disconnected. AudioObjectPropertyListenerBlock refreshNeededListener; // The devices we've added refreshNeededListener to. Used to avoid adding it to a device twice // for the same property and to remove it from all devices in dealloc. std::set listenedDevices_kAudioDevicePropertyDataSources; std::set listenedDevices_kAudioDevicePropertyDataSource; } - (instancetype) initWithBGMMenu:(NSMenu*)inBGMMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices preferredDevices:(BGMPreferredOutputDevices*)inPreferredDevices { if ((self = [super init])) { bgmMenu = inBGMMenu; audioDevices = inAudioDevices; preferredDevices = inPreferredDevices; outputDeviceMenuItems = [NSMutableArray new]; [self listenForDevicesAddedOrRemoved]; [self populateBGMMenu]; } return self; } - (void) dealloc { // Tell CoreAudio not to call the listener block anymore. This probably isn't necessary. // // I think it's safe to do this without dispatching to the main queue because dealloc and // refreshNeededListener should be essentially mutually exclusive. If refreshNeededListener is // invoked and gets a value for weakSelf, it holds the strong ref until it returns, so dealloc // won't be called. If refreshNeededListener is invoked and deallocation has started, it will // get nil for weakSelf and just return. auto removeListener = [&] (CAHALAudioObject audioObject, AudioObjectPropertySelector prop) { BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { // Check the object still exists first to reduce unnecessary error logs. if (CAHALAudioObject::ObjectExists(audioObject.GetObjectID())) { audioObject.RemovePropertyListenerBlock(CAPropertyAddress(prop), dispatch_get_main_queue(), refreshNeededListener); } }); }; // Remove the listener from each audio object we added it to. removeListener(CAHALAudioSystemObject(), kAudioHardwarePropertyDevices); for (auto device : listenedDevices_kAudioDevicePropertyDataSources) { removeListener(device, kAudioDevicePropertyDataSources); } for (auto device : listenedDevices_kAudioDevicePropertyDataSource) { removeListener(device, kAudioDevicePropertyDataSource); } } - (void) listenForDevicesAddedOrRemoved { // Create the block that will run when a device is added or removed. BGMOutputDeviceMenuSection* __weak weakSelf = self; refreshNeededListener = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) { #pragma unused (inNumberAddresses, inAddresses) BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { [weakSelf populateBGMMenu]; }); }; // Register the listener block to be called when devices are connected or disconnected. BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { CAHALAudioSystemObject().AddPropertyListenerBlock( CAPropertyAddress(kAudioHardwarePropertyDevices), dispatch_get_main_queue(), refreshNeededListener); }); } - (void) populateBGMMenu { // TODO: Technically, we should assert we're on the main queue rather than just the main thread. BGMAssert([NSThread isMainThread], "BGMOutputDeviceMenuSection::populateBGMMenu called on non-main thread"); // Remove existing menu items for (NSMenuItem* item in outputDeviceMenuItems) { DebugMsg("BGMOutputDeviceMenuSection::populateBGMMenu: Removing %s", item.description.UTF8String); [bgmMenu removeItem:item]; } [outputDeviceMenuItems removeAllObjects]; // Add a menu item for each output device CAHALAudioSystemObject audioSystem; UInt32 numDevices = audioSystem.GetNumberAudioDevices(); if (numDevices > 0) { CAAutoArrayDelete devices(numDevices); audioSystem.GetAudioDevices(numDevices, devices); for (UInt32 i = 0; i < numDevices; i++) { [self insertMenuItemsForDevice:devices[i]]; } } } - (void) insertMenuItemsForDevice:(BGMAudioDevice)device { // Insert menu items after the item for the "Output Device" heading. const NSInteger menuItemsIdx = [bgmMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1; BOOL canBeOutputDevice = YES; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { canBeOutputDevice = device.CanBeOutputDeviceInBGMApp(); }); if (canBeOutputDevice) { for (NSMenuItem* item : [self createMenuItemsForDevice:device]) { DebugMsg("BGMOutputDeviceMenuSection::insertMenuItemsForDevice: Inserting %s", item.description.UTF8String); [bgmMenu insertItem:item atIndex:menuItemsIdx]; [outputDeviceMenuItems addObject:item]; } // Add listeners to update the menu when the device's data source changes or it changes its // list of data sources. We do this so that, for example, when you plug headphones into the // built-in jack, the menu item for the built-in device will change from "Internal Speakers" // to "Headphones". if (listenedDevices_kAudioDevicePropertyDataSources.count(device) == 0) { BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { device.AddPropertyListenerBlock(CAPropertyAddress(kAudioDevicePropertyDataSources, kAudioDevicePropertyScopeOutput), dispatch_get_main_queue(), refreshNeededListener); listenedDevices_kAudioDevicePropertyDataSources.insert(device); }); }; if (listenedDevices_kAudioDevicePropertyDataSource.count(device) == 0) { BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { device.AddPropertyListenerBlock(CAPropertyAddress(kAudioDevicePropertyDataSource, kAudioDevicePropertyScopeOutput), dispatch_get_main_queue(), refreshNeededListener); listenedDevices_kAudioDevicePropertyDataSource.insert(device); }); }; } } - (NSArray*) createMenuItemsForDevice:(CAHALAudioDevice)device { // We fill this array with a menu item for each output device (or each data source for each device) on // the system. NSMutableArray* items = [NSMutableArray new]; AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput; UInt32 channel = kAudioObjectPropertyElementMaster; // If the device has data sources, create a menu item for each. Otherwise, create a single menu item // for the device. This way the menu items' titles will be, for example, "Internal Speakers" rather // than "Built-in Output". // // TODO: Handle data destinations as well? I don't have (or know of) any hardware with them. // TODO: Use the current data source's name when the control isn't settable, but only add one menu item. UInt32 numDataSources = 0; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { if (device.HasDataSourceControl(scope, channel) && device.DataSourceControlIsSettable(scope, channel)) { numDataSources = device.GetNumberAvailableDataSources(scope, channel); } }); if (numDataSources > 0) { std::vector dataSourceIDs(numDataSources); // This call updates numDataSources to the real number of IDs it added to our array. device.GetAvailableDataSources(scope, channel, numDataSources, dataSourceIDs.data()); for (UInt32 i = 0; i < numDataSources; i++) { DebugMsg("BGMOutputDeviceMenuSection::createMenuItemsForDevice: " "Creating item. %s%u %s%u", "Device ID:", device.GetObjectID(), ", Data source ID:", dataSourceIDs[i]); BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "(DS)", [&] { NSString* dataSourceName = CFBridgingRelease(device.CopyDataSourceNameForID(scope, channel, dataSourceIDs[i])); NSString* deviceName = CFBridgingRelease(device.CopyName()); [items addObject:[self createMenuItemForDevice:device dataSourceID:@(dataSourceIDs[i]) title:dataSourceName toolTip:deviceName]]; }); } } else { DebugMsg("BGMOutputDeviceMenuSection::createMenuItemsForDevice: Creating item. %s%u", "Device ID:", device.GetObjectID()); BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { [items addObject:[self createMenuItemForDevice:device dataSourceID:nil title:CFBridgingRelease(device.CopyName()) toolTip:nil]]; }); } return items; } - (NSMenuItem*) createMenuItemForDevice:(CAHALAudioDevice)device dataSourceID:(NSNumber* __nullable)dataSourceID title:(NSString* __nullable)title toolTip:(NSString* __nullable)toolTip { // If we don't have a title, use the tool-tip text instead. if (!title) { title = (toolTip ? toolTip : @""); } NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:BGMNN(title) action:@selector(outputDeviceMenuItemSelected:) keyEquivalent:@""]; // Add the AirPlay icon to the labels of AirPlay devices. // // TODO: Test this with real hardware that supports AirPlay. (I don't have any.) BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { if (device.GetTransportType() == kAudioDeviceTransportTypeAirPlay) { item.image = [NSImage imageNamed:@"AirPlayIcon"]; // Make the icon a "template image" so it gets drawn colour-inverted when it's highlighted or // OS X is in dark mode. [item.image setTemplate:YES]; } }); // The menu item should be selected if it's the menu item for the current output device. If the device // has data sources, only the menu item for the current data source should be selected. BOOL isSelected = [audioDevices isOutputDevice:device.GetObjectID()] && (!dataSourceID || [audioDevices isOutputDataSource:[dataSourceID unsignedIntValue]]); item.state = (isSelected ? NSOnState : NSOffState); item.toolTip = toolTip; item.target = self; item.indentationLevel = 1; item.representedObject = @{ @"deviceID": @(device.GetObjectID()), @"dataSourceID": dataSourceID ? BGMNN(dataSourceID) : [NSNull null] }; #if __clang_major__ >= 9 if (@available(macOS 10.10, *)) { // Used for UI tests. item.accessibilityIdentifier = @"output-device"; } #endif return item; } // Called by BGMAudioDeviceManager to tell us a different device has been set as the output device. - (void) outputDeviceDidChange { BGMOutputDeviceMenuSection* __weak weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { [weakSelf populateBGMMenu]; }); }); } - (void) outputDeviceMenuItemSelected:(NSMenuItem*)menuItem { DebugMsg("BGMOutputDeviceMenuSection::outputDeviceMenuItemSelected: '%s' menu item selected", [menuItem.title UTF8String]); // Make sure the menu item is actually for an output device. if (![outputDeviceMenuItems containsObject:menuItem]) { return; } // Change to the new output device. AudioDeviceID newDeviceID = [[menuItem representedObject][@"deviceID"] unsignedIntValue]; id newDataSourceID = [menuItem representedObject][@"dataSourceID"]; BOOL changingDevice = ![audioDevices isOutputDevice:newDeviceID]; BOOL changingDataSource = (newDataSourceID != [NSNull null]) && ![audioDevices isOutputDataSource:[newDataSourceID unsignedIntValue]]; if (changingDevice || changingDataSource) { NSString* deviceName = menuItem.toolTip ? [NSString stringWithFormat:@"%@ (%@)", menuItem.title, menuItem.toolTip] : menuItem.title; if (changingDevice) { // Add the new output device to the list of preferred devices. [preferredDevices userChangedOutputDeviceTo:newDeviceID]; } // Dispatched because it usually blocks. (Note that we're using // DISPATCH_QUEUE_PRIORITY_HIGH, which is the second highest priority.) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self changeToOutputDevice:newDeviceID newDataSource:newDataSourceID deviceName:deviceName]; }); } } - (void) changeToOutputDevice:(AudioDeviceID)deviceID newDataSource:(id)dataSourceID deviceName:(NSString*)deviceName { NSError* __nullable error; if (dataSourceID == [NSNull null]) { error = [audioDevices setOutputDeviceWithID:deviceID revertOnFailure:YES]; } else { error = [audioDevices setOutputDeviceWithID:deviceID dataSourceID:[dataSourceID unsignedIntValue] revertOnFailure:YES]; } if (error) { // Couldn't change the output device, so show a warning. (No need to change the menu // selection back because it gets repopulated every time it's opened.) // NSAlerts should only be shown on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"Failed to set output device: %@", deviceName); NSAlert* alert = [NSAlert new]; alert.messageText = [NSString stringWithFormat:@"Failed to set %@ as the output device.", deviceName]; alert.informativeText = @"This is probably a bug. Feel free to report it."; [alert runModal]; }); } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMOutputVolumeMenuItem.h ================================================ // 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 . // // BGMOutputVolumeMenuItem.h // BGMApp // // Copyright © 2017 Kyle Neideck // // Local Includes #import "BGMAudioDeviceManager.h" // System Includes #import #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 ================================================ FILE: BGMApp/BGMApp/BGMOutputVolumeMenuItem.mm ================================================ // 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 . // // BGMOutputVolumeMenuItem.mm // BGMApp // // Copyright © 2017-2019 Kyle Neideck // // Self Include #import "BGMOutputVolumeMenuItem.h" // Local Includes #import "BGM_Utils.h" #import "BGMAudioDevice.h" #import "BGMVolumeChangeListener.h" // PublicUtility Includes #import "CAException.h" #import "CAPropertyAddress.h" // System Includes #import #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; BGMVolumeChangeListener* volumeChangeListener; 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; // volumeChangeListener and updateLabelListenerBlock are initialised in the methods called // below. // Apply our custom view from MainMenu.xib. self.view = view; // Set up the UI components in the view. [self initSlider]; [self updateLabelAndToolTip]; // Register a listener so we can update if the output device's data source changes. [self addOutputDeviceDataSourceListener]; } return self; } - (void) dealloc { // Remove the audio property listeners. // TODO: This call isn't thread safe. (But currently this dealloc method is only called if // there's an error.) [self removeOutputDeviceDataSourceListener]; } - (void) initSlider { BGMAssert([NSThread isMainThread], "initSlider must be called from the main thread because it calls UI functions."); 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; volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [=] { [weakSelf updateVolumeSlider]; }); } // 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", ([&] { // Technically, there's a race here in that the device could be removed after we check it // exists, but before we try to remove the listener. We could check the error code of the // exception and not log an error message if the code is kAudioHardwareBadObjectError or // kAudioHardwareBadDeviceError, but it probably wouldn't be worth the effort. // // So for now the main reason for checking the device exists here is that it makes debug // builds much less likely to crash here. (They crash/break when an error is logged so it // will be noticed.) if (CAHALAudioObject::ObjectExists(outputDevice)) { outputDevice.RemovePropertyListenerBlock( CAPropertyAddress(kAudioDevicePropertyDataSource, kScope), dispatch_get_main_queue(), updateLabelListenerBlock); } })); } - (void) outputDeviceDidChange { dispatch_async(dispatch_get_main_queue(), ^{ // Remove the data source listener from the previous output device. [self removeOutputDeviceDataSourceListener]; // Add it to the new output device. outputDevice = audioDevices.outputDevice; [self addOutputDeviceDataSourceListener]; // Update the label to use the name of the new output device. [self updateLabelAndToolTip]; // Set the slider to the volume of the new device. [self updateVolumeSlider]; }); } // 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 { if (outputDevice.GetObjectID() == kAudioObjectUnknown) { DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Output device unknown. Using the " "generic label."); self.toolTip = nil; deviceLabel.stringValue = kGenericOutputDeviceName; } else { BOOL didSetLabel = NO; DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Output device: %u", outputDevice.GetObjectID()); try { if (outputDevice.HasDataSourceControl(kScope, kMasterChannel)) { DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Getting data source ID"); // The device has datasources, so use the current datasource's name like macOS does. UInt32 dataSourceID = outputDevice.GetCurrentDataSourceID(kScope, kMasterChannel); DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: " "Getting name for data source %u", dataSourceID); deviceLabel.stringValue = (__bridge_transfer NSString*)outputDevice.CopyDataSourceNameForID( kScope, kMasterChannel, dataSourceID); // So we know not to change the text if setting the tooltip fails. didSetLabel = YES; DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Getting device name"); // 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 { DebugMsg("BGMOutputVolumeMenuItem::updateLabelAndToolTip: Getting device name"); 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 ================================================ FILE: BGMApp/BGMApp/BGMPlayThrough.cpp ================================================ // 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 . // // BGMPlayThrough.cpp // BGMApp // // Copyright © 2016, 2017, 2020 Kyle Neideck // // Self Include #include "BGMPlayThrough.h" // Local Includes #include "BGM_Types.h" #include "BGM_Utils.h" // PublicUtility Includes #include "CAHALAudioSystemObject.h" #include "CAPropertyAddress.h" // STL Includes #include // For std::max // System Includes #include #include #include // The number of IO cycles (roughly) to wait for our IOProcs to stop themselves before assuming something // went wrong. If that happens, we try to stop them from a non-IO thread and continue anyway. static const UInt32 kStopIOProcTimeoutInIOCycles = 600; #pragma mark Construction/Destruction BGMPlayThrough::BGMPlayThrough(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice) : mInputDevice(inInputDevice), mOutputDevice(inOutputDevice) { Init(inInputDevice, inOutputDevice); } BGMPlayThrough::~BGMPlayThrough() { CAMutex::Locker stateLocker(mStateMutex); BGMLogAndSwallowExceptionsMsg("BGMPlayThrough::~BGMPlayThrough", "Deactivate", [&]() { Deactivate(); }); // If one of the IOProcs failed to stop, CoreAudio could (at least in theory) still call it // after this point. This isn't a solution, but calling DeallocateBuffer instead of letting it // deallocate itself should at least make the error less likely to cause a segfault, since // DeallocateBuffer takes the buffer locks and sets mBuffer to null. // // TODO: It probably wouldn't be too hard to fix this properly by giving the IOProcs weak refs // to the BGMPlayThrough object instead of raw pointers. DeallocateBuffer(); if(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL) { kern_return_t theError = semaphore_destroy(mach_task_self(), mOutputDeviceIOProcSemaphore); BGM_Utils::LogIfMachError("BGMPlayThrough::~BGMPlayThrough", "semaphore_destroy", theError); } } void BGMPlayThrough::Init(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice) { BGMAssert(mInputDeviceIOProcState.is_lock_free(), "BGMPlayThrough::BGMPlayThrough: !mInputDeviceIOProcState.is_lock_free()"); BGMAssert(mOutputDeviceIOProcState.is_lock_free(), "BGMPlayThrough::BGMPlayThrough: !mOutputDeviceIOProcState.is_lock_free()"); BGMAssert(!mActive, "BGMPlayThrough::BGMPlayThrough: Can't init while active."); mInputDevice = inInputDevice; mOutputDevice = inOutputDevice; AllocateBuffer(); try { // Init the semaphore for the output IOProc. if(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL) { kern_return_t theError = semaphore_create(mach_task_self(), &mOutputDeviceIOProcSemaphore, SYNC_POLICY_FIFO, 0); BGM_Utils::ThrowIfMachError("BGMPlayThrough::BGMPlayThrough", "semaphore_create", theError); ThrowIf(mOutputDeviceIOProcSemaphore == SEMAPHORE_NULL, CAException(kAudioHardwareUnspecifiedError), "BGMPlayThrough::BGMPlayThrough: Could not create semaphore"); } } catch (...) { // Clean up. DeallocateBuffer(); throw; } } void BGMPlayThrough::Activate() { CAMutex::Locker stateLocker(mStateMutex); if(!mActive) { DebugMsg("BGMPlayThrough::Activate: Activating playthrough"); CreateIOProcIDs(); mActive = true; // TODO: This code (the next two blocks) should be in BGMDeviceControlSync. // Set BGMDevice's sample rate to match the output device. try { Float64 outputSampleRate = mOutputDevice.GetNominalSampleRate(); mInputDevice.SetNominalSampleRate(outputSampleRate); } catch (CAException e) { LogWarning("BGMPlayThrough::Activate: Failed to sync device sample rates. Error: %d", e.GetError()); } // Set BGMDevice's IO buffer size to match the output device. try { UInt32 outputBufferSize = mOutputDevice.GetIOBufferSize(); mInputDevice.SetIOBufferSize(outputBufferSize); } catch (CAException e) { LogWarning("BGMPlayThrough::Activate: Failed to sync device buffer sizes. Error: %d", e.GetError()); } DebugMsg("BGMPlayThrough::Activate: Registering for notifications from BGMDevice."); mInputDevice.AddPropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning), &BGMPlayThrough::BGMDeviceListenerProc, this); mInputDevice.AddPropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload), &BGMPlayThrough::BGMDeviceListenerProc, this); bool isBGMDevice = true; CATry isBGMDevice = mInputDevice.IsBGMDeviceInstance(); CACatch if(isBGMDevice) { mInputDevice.AddPropertyListener(kBGMRunningSomewhereOtherThanBGMAppAddress, &BGMPlayThrough::BGMDeviceListenerProc, this); } else { 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: !mInputDevice.IsBGMDeviceInstance()"); } } } void BGMPlayThrough::Deactivate() { CAMutex::Locker stateLocker(mStateMutex); if(mActive) { DebugMsg("BGMPlayThrough::Deactivate: Deactivating playthrough"); bool inputDeviceIsBGMDevice = true; CATry inputDeviceIsBGMDevice = mInputDevice.IsBGMDeviceInstance(); CACatch // Unregister notification listeners. if(inputDeviceIsBGMDevice) { // There's not much we can do if these calls throw. The docs for AudioObjectRemovePropertyListener // just say that means it failed. BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] { mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning), &BGMPlayThrough::BGMDeviceListenerProc, this); }); BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] { mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload), &BGMPlayThrough::BGMDeviceListenerProc, this); }); BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] { mInputDevice.RemovePropertyListener(kBGMRunningSomewhereOtherThanBGMAppAddress, &BGMPlayThrough::BGMDeviceListenerProc, this); }); } BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] { Stop(); }); BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&] { DestroyIOProcIDs(); }); mActive = false; } } void BGMPlayThrough::AllocateBuffer() { // Allocate the ring buffer that will hold the data passing between the devices UInt32 numberStreams = 1; AudioStreamBasicDescription outputFormat[1]; mOutputDevice.GetCurrentVirtualFormats(false, numberStreams, outputFormat); if(numberStreams < 1) { Throw(CAException(kAudioHardwareUnsupportedOperationError)); } // Need to lock the buffer mutexes to make sure the IOProcs aren't accessing it. The order is // important here. We always lock them in the same order to prevent deadlocks. CAMutex::Locker lockerInput(mBufferInputMutex); CAMutex::Locker lockerOutput(mBufferOutputMutex); mBuffer = std::unique_ptr(new CARingBuffer); // The calculation for the size of the buffer is from Apple's CAPlayThrough.cpp sample code // // TODO: Test playthrough with hardware with more than 2 channels per frame, a sample (virtual) format other than // 32-bit floats and/or an IO buffer size other than 512 frames mBuffer->Allocate(outputFormat[0].mChannelsPerFrame, outputFormat[0].mBytesPerFrame, mOutputDevice.GetIOBufferSize() * 20); } void BGMPlayThrough::DeallocateBuffer() { // Need to lock the buffer mutexes to make sure the IOProcs aren't accessing it. The order is // important here. We always lock them in the same order to prevent deadlocks. CAMutex::Locker lockerInput(mBufferInputMutex); CAMutex::Locker lockerOutput(mBufferOutputMutex); mBuffer = nullptr; // Note that the buffer's destructor will deallocate it. } void BGMPlayThrough::CreateIOProcIDs() { CAMutex::Locker stateLocker(mStateMutex); BGMAssert(!mPlayingThrough, "BGMPlayThrough::CreateIOProcIDs: Tried to create IOProcs when playthrough was already running"); BGMAssert(mInputDeviceIOProcID == nullptr, "BGMPlayThrough::CreateIOProcIDs: mInputDeviceIOProcID must be destroyed first."); BGMAssert(mOutputDeviceIOProcID == nullptr, "BGMPlayThrough::CreateIOProcIDs: mOutputDeviceIOProcID must be destroyed first."); BGMAssert(CheckIOProcsAreStopped(), "BGMPlayThrough::CreateIOProcIDs: IOProcs not ready."); const bool inDeviceAlive = mInputDevice.IsAlive(); const bool outDeviceAlive = mOutputDevice.IsAlive(); if(inDeviceAlive && outDeviceAlive) { DebugMsg("BGMPlayThrough::CreateIOProcIDs: Creating IOProcs"); try { mInputDeviceIOProcID = mInputDevice.CreateIOProcID(&BGMPlayThrough::InputDeviceIOProc, this); } catch(CAException e) { LogWarning("BGMPlayThrough::CreateIOProcIDs: Failed to create input IOProc ID. mInputDevice = %d", mInputDevice.GetObjectID()); throw; } try { mOutputDeviceIOProcID = mOutputDevice.CreateIOProcID(&BGMPlayThrough::OutputDeviceIOProc, this); } catch(CAException e) { LogWarning("BGMPlayThrough::CreateIOProcIDs: Failed to create output IOProc ID. mOutputDevice = %d", mOutputDevice.GetObjectID()); DestroyIOProcIDs(); // Clean up. throw; } if(mInputDeviceIOProcID == nullptr || mOutputDeviceIOProcID == nullptr) { // Should never happen if CAHALAudioDevice::CreateIOProcID didn't throw. LogError("BGMPlayThrough::CreateIOProcIDs: Null IOProc ID returned by CreateIOProcID"); throw new CAException(kAudioHardwareIllegalOperationError); } // TODO: Try using SetIOCycleUsage to reduce latency? Our IOProcs don't really do anything except copy a small // buffer. According to this, Jack OS X considered it: // https://lists.apple.com/archives/coreaudio-api/2008/Mar/msg00043.html but from a quick look at their // code, I don't think they ended up using it. // mInputDevice->SetIOCycleUsage(0.01f); // mOutputDevice->SetIOCycleUsage(0.01f); } else { LogWarning("BGMPlayThrough::CreateIOProcIDs: Failed to create IOProcs.%s%s", (inDeviceAlive ? "" : " Input device not alive."), (outDeviceAlive ? "" : " Output device not alive.")); throw new CAException(kAudioHardwareIllegalOperationError); } } void BGMPlayThrough::DestroyIOProcIDs() { CAMutex::Locker stateLocker(mStateMutex); // In release builds, we still try to destroy the IDs if the IOProcs are running, hoping they just haven't been // stopped quite yet. The docs for AudioDeviceDestroyIOProcID don't say not to do that, but it could cause races // if one really is still running so it isn't ideal. BGMAssert(CheckIOProcsAreStopped(), "BGMPlayThrough::DestroyIOProcIDs: IOProcs not ready."); DebugMsg("BGMPlayThrough::DestroyIOProcIDs: Destroying IOProcs"); auto destroy = [](BGMAudioDevice& device, const char* deviceName, AudioDeviceIOProcID& ioProcID) { #if !DEBUG #pragma unused (deviceName) #endif if(ioProcID != nullptr) { try { device.DestroyIOProcID(ioProcID); } catch(CAException e) { if((e.GetError() == kAudioHardwareBadDeviceError) || (e.GetError() == kAudioHardwareBadObjectError)) { // This means the IOProc IDs will have already been destroyed, so there's nothing to do. DebugMsg("BGMPlayThrough::DestroyIOProcIDs: Didn't destroy IOProc ID for %s device because " "it's not connected anymore. deviceID = %d", deviceName, device.GetObjectID()); } else { ioProcID = nullptr; throw; } } ioProcID = nullptr; } }; destroy(mInputDevice, "input", mInputDeviceIOProcID); destroy(mOutputDevice, "output", mOutputDeviceIOProcID); } bool BGMPlayThrough::CheckIOProcsAreStopped() const noexcept { bool statesOK = true; if(mInputDeviceIOProcState != IOState::Stopped) { LogWarning("BGMPlayThrough::CheckIOProcsAreStopped: Input IOProc not stopped. mInputDeviceIOProcState = %d", mInputDeviceIOProcState.load()); statesOK = false; } if(mOutputDeviceIOProcState != IOState::Stopped) { LogWarning("BGMPlayThrough::CheckIOProcsAreStopped: Output IOProc not stopped. mOutputDeviceIOProcState = %d", mOutputDeviceIOProcState.load()); statesOK = false; } return statesOK; } void BGMPlayThrough::SetDevices(const BGMAudioDevice* __nullable inInputDevice, const BGMAudioDevice* __nullable inOutputDevice) { CAMutex::Locker stateLocker(mStateMutex); bool wasActive = mActive; bool wasPlayingThrough = mPlayingThrough; if(wasPlayingThrough) { BGMAssert(wasActive, "BGMPlayThrough::SetOutputDevice: wasPlayingThrough && !wasActive"); // Sanity check. } Deactivate(); mInputDevice = inInputDevice ? *inInputDevice : mInputDevice; mOutputDevice = inOutputDevice ? *inOutputDevice : mOutputDevice; // Resize and reallocate the buffer if necessary. Init(mInputDevice, mOutputDevice); if(wasActive) { Activate(); } if(wasPlayingThrough) { Start(); } } #pragma mark Control Playthrough void BGMPlayThrough::Start() { CAMutex::Locker stateLocker(mStateMutex); if(mPlayingThrough) { DebugMsg("BGMPlayThrough::Start: Already started/starting."); if(mOutputDeviceIOProcState == IOState::Running) { ReleaseThreadsWaitingForOutputToStart(); } return; } if(!mInputDevice.IsAlive() || !mOutputDevice.IsAlive()) { LogError("BGMPlayThrough::Start: %s %s", mInputDevice.IsAlive() ? "" : "!mInputDevice", mOutputDevice.IsAlive() ? "" : "!mOutputDevice"); ReleaseThreadsWaitingForOutputToStart(); throw CAException(kAudioHardwareBadDeviceError); } // Set up IOProcs and listeners if they haven't been already. Activate(); BGMAssert((mInputDeviceIOProcID != nullptr) && (mOutputDeviceIOProcID != nullptr), "BGMPlayThrough::Start: Null IOProc ID"); if((mInputDeviceIOProcState != IOState::Stopped) || (mOutputDeviceIOProcState != IOState::Stopped)) { LogWarning("BGMPlayThrough::Start: IOProc(s) not ready. Trying to start anyway. %s%d %s%d", "mInputDeviceIOProcState = ", mInputDeviceIOProcState.load(), "mOutputDeviceIOProcState = ", mOutputDeviceIOProcState.load()); } DebugMsg("BGMPlayThrough::Start: Starting playthrough"); // Start our IOProcs. try { mInputDeviceIOProcState = IOState::Starting; mInputDevice.StartIOProc(mInputDeviceIOProcID); mOutputDeviceIOProcState = IOState::Starting; mOutputDevice.StartIOProc(mOutputDeviceIOProcID); } catch(CAException e) { ReleaseThreadsWaitingForOutputToStart(); // Log an error message. OSStatus err = e.GetError(); char err4CC[5] = CA4CCToCString(err); LogError("BGMPlayThrough::Start: Failed to start %s device. Error: %d (%s)", (mOutputDeviceIOProcState == IOState::Starting ? "output" : "input"), err, err4CC); // Try to stop the IOProcs in case StartIOProc failed because one of our IOProc was already // running. I don't know if it actually does fail in that case, but the documentation // doesn't say so it's safer to assume it could. CATry mInputDevice.StopIOProc(mInputDeviceIOProcID); CACatch CATry mOutputDevice.StopIOProc(mOutputDeviceIOProcID); CACatch mInputDeviceIOProcState = IOState::Stopped; mOutputDeviceIOProcState = IOState::Stopped; throw; } mPlayingThrough = true; } OSStatus BGMPlayThrough::WaitForOutputDeviceToStart() noexcept { // Check for errors. // // Technically we should take the state mutex here, but that could cause deadlocks because // BGM_Device::StartIO (in BGMDriver) blocks on this function (via XPC). Other BGMPlayThrough // functions make requests to BGMDriver while holding the state mutex, usually to get/set // properties, but the HAL will block those requests until BGM_Device::StartIO returns. try { if(!mActive) { LogError("BGMPlayThrough::WaitForOutputDeviceToStart: !mActive"); return kAudioHardwareNotRunningError; } if(!mOutputDevice.IsAlive()) { LogError("BGMPlayThrough::WaitForOutputDeviceToStart: Device not alive"); return kAudioHardwareBadDeviceError; } } catch(const CAException& e) { BGMLogException(e); return e.GetError(); } const IOState initialState = mOutputDeviceIOProcState; const UInt64 startedAt = mach_absolute_time(); if(initialState == IOState::Running) { // Return early because the output device is already running. return kAudioHardwareNoError; } else if(initialState != IOState::Starting) { // Warn if we haven't been told to start the output device yet. Usually means we // haven't received a kAudioDevicePropertyDeviceIsRunning notification yet, which can // happen. It's most common when the user changes the output device while IO is // running. LogWarning("BGMPlayThrough::WaitForOutputDeviceToStart: Device not starting"); return kDeviceNotStarting; } // Wait for our output IOProc to start. mOutputDeviceIOProcSemaphore is reset to 0 // (semaphore_signal_all) when our IOProc is running on the output device. // // This does mean that we won't have any data the first time our IOProc is called, but I // don't know any way to wait until just before that point. (The device's IsRunning property // changes immediately after we call StartIOProc.) // // We check mOutputDeviceIOProcState every 200ms as a fault tolerance mechanism. (Though, // I'm not completely sure it's impossible to miss the signal from the IOProc because of a // spurious wake up, so it might actually be necessary.) DebugMsg("BGMPlayThrough::WaitForOutputDeviceToStart: Waiting."); kern_return_t theError; IOState state; UInt64 waitedNsec = 0; mach_timebase_info_data_t info; mach_timebase_info(&info); do { BGMAssert(mOutputDeviceIOProcSemaphore != SEMAPHORE_NULL, "BGMPlayThrough::WaitForOutputDeviceToStart: !mOutputDeviceIOProcSemaphore"); theError = semaphore_timedwait(mOutputDeviceIOProcSemaphore, (mach_timespec_t){ 0, 200 * NSEC_PER_MSEC }); // Update the total time we've been waiting and the output device's state. waitedNsec = (mach_absolute_time() - startedAt) * info.numer / info.denom; state = mOutputDeviceIOProcState; } while((theError != KERN_SUCCESS) && // Signalled from the IOProc. (state == IOState::Starting) && // IO state changed. (waitedNsec < kStartIOTimeoutNsec)); // Timed out. if(BGMDebugLoggingIsEnabled()) { UInt64 startedBy = mach_absolute_time(); struct mach_timebase_info baseInfo = { 0, 0 }; mach_timebase_info(&baseInfo); UInt64 base = baseInfo.numer / baseInfo.denom; DebugMsg("BGMPlayThrough::WaitForOutputDeviceToStart: Started %f ms after notification, %f " "ms after entering WaitForOutputDeviceToStart.", static_cast(startedBy - mToldOutputDeviceToStartAt) * base / NSEC_PER_MSEC, static_cast(startedBy - startedAt) * base / NSEC_PER_MSEC); } // Figure out which error code to return. switch (theError) { case KERN_SUCCESS: // Signalled from the IOProc. return kAudioHardwareNoError; // IO state changed or we timed out after case KERN_OPERATION_TIMED_OUT: // - semaphore_timedwait timed out, or case KERN_ABORTED: // - a spurious wake-up. return (state == IOState::Running) ? kAudioHardwareNoError : kAudioHardwareNotRunningError; default: BGM_Utils::LogIfMachError("BGMPlayThrough::WaitForOutputDeviceToStart", "semaphore_timedwait", theError); return kAudioHardwareUnspecifiedError; } } // Release any threads waiting for the output device to start. This function doesn't take mStateMutex // because it gets called on the IO thread, which is realtime priority. void BGMPlayThrough::ReleaseThreadsWaitingForOutputToStart() { if(mActive) { semaphore_t semaphore = mOutputDeviceIOProcSemaphore; if(semaphore != SEMAPHORE_NULL) { mRTLogger.LogReleasingWaitingThreads(); kern_return_t theError = semaphore_signal_all(semaphore); mRTLogger.LogIfMachError_ReleaseWaitingThreadsSignal(theError); } } } OSStatus BGMPlayThrough::Stop() { CAMutex::Locker stateLocker(mStateMutex); // TODO: Tell the waiting threads what happened so they can return an error? ReleaseThreadsWaitingForOutputToStart(); if(mActive && mPlayingThrough) { DebugMsg("BGMPlayThrough::Stop: Stopping playthrough"); bool inputDeviceAlive = false; bool outputDeviceAlive = false; CATry inputDeviceAlive = CAHALAudioObject::ObjectExists(mInputDevice) && mInputDevice.IsAlive(); CACatch CATry outputDeviceAlive = CAHALAudioObject::ObjectExists(mOutputDevice) && mOutputDevice.IsAlive(); CACatch mInputDeviceIOProcState = inputDeviceAlive ? IOState::Stopping : IOState::Stopped; mOutputDeviceIOProcState = outputDeviceAlive ? IOState::Stopping : IOState::Stopped; // Wait for the IOProcs to stop themselves. This is so the IOProcs don't get called after the BGMPlayThrough instance // (pointed to by the client data they get from the HAL) is deallocated. // // From Jeff Moore on the Core Audio mailing list: // Note that there is no guarantee about how many times your IOProc might get called after AudioDeviceStop() returns // when you make the call from outside of your IOProc. However, if you call AudioDeviceStop() from inside your IOProc, // you do get the guarantee that your IOProc will not get called again after the IOProc has returned. UInt64 totalWaitNs = 0; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&]() { Float64 expectedInputCycleNs = 0; if(inputDeviceAlive) { expectedInputCycleNs = mInputDevice.GetIOBufferSize() * (1 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC; } Float64 expectedOutputCycleNs = 0; if(outputDeviceAlive) { expectedOutputCycleNs = mOutputDevice.GetIOBufferSize() * (1 / mOutputDevice.GetNominalSampleRate()) * NSEC_PER_SEC; } UInt64 expectedMaxCycleNs = static_cast(std::max(expectedInputCycleNs, expectedOutputCycleNs)); while((mInputDeviceIOProcState == IOState::Stopping || mOutputDeviceIOProcState == IOState::Stopping) && (totalWaitNs < kStopIOProcTimeoutInIOCycles * expectedMaxCycleNs)) { // TODO: If playthrough is started again while we're waiting in this loop we could drop frames. Wait on a // semaphore instead of sleeping? That way Start() could also signal it, before waiting on the state mutex, // as a way of cancelling the stop operation. struct timespec rmtp; int err = nanosleep((const struct timespec[]){{0, NSEC_PER_MSEC}}, &rmtp); totalWaitNs += NSEC_PER_MSEC - (err == -1 ? rmtp.tv_nsec : 0); } }); // Clean up if the IOProcs didn't stop themselves if(mInputDeviceIOProcState == IOState::Stopping && mInputDeviceIOProcID != nullptr) { LogError("BGMPlayThrough::Stop: The input IOProc didn't stop itself in time. Stopping " "it from outside of the IO thread."); BGMLogUnexpectedExceptions("BGMPlayThrough::Stop", [&]() { mInputDevice.StopIOProc(mInputDeviceIOProcID); }); mInputDeviceIOProcState = IOState::Stopped; } if(mOutputDeviceIOProcState == IOState::Stopping && mOutputDeviceIOProcID != nullptr) { LogError("BGMPlayThrough::Stop: The output IOProc didn't stop itself in time. Stopping " "it from outside of the IO thread."); BGMLogUnexpectedExceptions("BGMPlayThrough::Stop", [&]() { mOutputDevice.StopIOProc(mOutputDeviceIOProcID); }); mOutputDeviceIOProcState = IOState::Stopped; } mPlayingThrough = false; } mFirstInputSampleTime = -1; mLastInputSampleTime = -1; mLastOutputSampleTime = -1; return noErr; // TODO: Why does this return anything and why always noErr? } void BGMPlayThrough::StopIfIdle() { // To save CPU time, we stop playthrough when no clients are doing IO. This should reduce the coreaudiod and BGMApp // processes' idle CPU use to virtually none. If this isn't working for you, a client might be running IO without // being audible. VLC does that when you have a file paused, for example. CAMutex::Locker stateLocker(mStateMutex); BGMAssert(mInputDevice.IsBGMDeviceInstance(), "BGMDevice not set as input device. StopIfIdle can't tell if other devices are idle."); if(!IsRunningSomewhereOtherThanBGMApp(mInputDevice)) { mLastNotifiedIOStoppedOnBGMDevice = mach_absolute_time(); // Wait a bit before stopping playthrough. // // This keeps us from starting and stopping IO too rapidly, which wastes CPU, and gives BGMDriver time to update // kAudioDeviceCustomPropertyDeviceAudibleState, which it can only do while IO is running. (The wait duration is // more or less arbitrary, except that it has to be longer than kDeviceAudibleStateMinChangedFramesForUpdate.) // 1 / sample rate = seconds per frame Float64 nsecPerFrame = (1.0 / mInputDevice.GetNominalSampleRate()) * NSEC_PER_SEC; UInt64 waitNsec = static_cast(20 * kDeviceAudibleStateMinChangedFramesForUpdate * nsecPerFrame); UInt64 queuedAt = mLastNotifiedIOStoppedOnBGMDevice; DebugMsg("BGMPlayThrough::StopIfIdle: Will dispatch stop-if-idle block in %llu ns. %s%llu", waitNsec, "queuedAt=", queuedAt); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, waitNsec), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // Check the BGMPlayThrough instance hasn't been destructed since it queued this block if(mActive) { // The "2" is just to avoid shadowing the other locker CAMutex::Locker stateLocker2(mStateMutex); // Don't stop playthrough if IO has started running again or if // kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp has changed since // this block was queued if(mPlayingThrough && !IsRunningSomewhereOtherThanBGMApp(mInputDevice) && queuedAt == mLastNotifiedIOStoppedOnBGMDevice) { DebugMsg("BGMPlayThrough::StopIfIdle: BGMDevice is only running IO for BGMApp. " "Stopping playthrough."); Stop(); } } }); } } #pragma mark BGMDevice Listener // TODO: Listen for changes to the sample rate and IO buffer size of the output device and update the input device to match // static OSStatus BGMPlayThrough::BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* __nonnull inAddresses, void* __nullable inClientData) { // refCon (reference context) is the instance that registered the listener proc BGMPlayThrough* refCon = static_cast(inClientData); // If the input device isn't BGMDevice, this listener proc shouldn't be registered ThrowIf(inObjectID != refCon->mInputDevice.GetObjectID(), CAException(kAudioHardwareBadObjectError), "BGMPlayThrough::BGMDeviceListenerProc: notified about audio object other than BGMDevice"); for(int i = 0; i < inNumberAddresses; i++) { switch(inAddresses[i].mSelector) { case kAudioDeviceProcessorOverload: // These warnings are common when you use the UI if you're running a debug build or have "Debug executable" // checked. You shouldn't be seeing them otherwise. DebugMsg("BGMPlayThrough::BGMDeviceListenerProc: WARNING! Got kAudioDeviceProcessorOverload notification"); LogWarning("Background Music: CPU overload reported\n"); break; // Start playthrough when a client starts IO on BGMDevice and stop when BGMApp (i.e. playthrough itself) is // the only client left doing IO. // // These cases are dispatched to avoid causing deadlocks by triggering one of the following notifications in // the process of handling one. Deadlocks could happen if these were handled synchronously when: // - the first BGMDeviceListenerProc call takes the state mutex, then requests some data from the HAL and // waits for it to return, // - the request triggers the HAL to send notifications, which it sends on a different thread, // - the HAL waits for the second BGMDeviceListenerProc call to return before it returns the data // requested by the first BGMDeviceListenerProc call, and // - the second BGMDeviceListenerProc call waits for the first to unlock the state mutex. case kAudioDevicePropertyDeviceIsRunning: // Received on the IO thread before our IOProc is called HandleBGMDeviceIsRunning(refCon); break; case kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp: HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(refCon); break; default: // We might get properties we didn't ask for, so we just ignore them. break; } } // From AudioHardware.h: "The return value is currently unused and should always be 0." return 0; } // static void BGMPlayThrough::HandleBGMDeviceIsRunning(BGMPlayThrough* refCon) { DebugMsg("BGMPlayThrough::HandleBGMDeviceIsRunning: Got notification"); // This is dispatched because it can block and // - we might be on a real-time thread, or // - 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(BGMGetDispatchQueue_PriorityUserInteractive(), ^{ if(refCon->mActive) { CAMutex::Locker stateLocker(refCon->mStateMutex); // Set to true initially because if we fail to get this property from BGMDevice we want to // 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. isRunningSomewhereOtherThanBGMApp = IsRunningSomewhereOtherThanBGMApp(refCon->mInputDevice); }); DebugMsg("BGMPlayThrough::HandleBGMDeviceIsRunning: " "BGMDevice is %srunning somewhere other than BGMApp", isRunningSomewhereOtherThanBGMApp ? "" : "not "); if(isRunningSomewhereOtherThanBGMApp) { refCon->mToldOutputDeviceToStartAt = mach_absolute_time(); // TODO: Handle expected exceptions (mostly CAExceptions from PublicUtility classes) in Start. // For any that can't be handled sensibly in Start, catch them here and retry a few // times (with a very short delay) before handling them by showing an unobtrusive error // message or something. Then try a different device or just set the system device back // to the real device. BGMLogAndSwallowExceptions("HandleBGMDeviceIsRunning", [&refCon]() { refCon->Start(); }); } } }); } // static void BGMPlayThrough::HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlayThrough* refCon) { 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(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // TODO: Handle expected exceptions (mostly CAExceptions from PublicUtility classes) in StopIfIdle. BGMLogUnexpectedExceptions("HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp", [&refCon]() { if(refCon->mActive) { refCon->StopIfIdle(); } }); }); } // static bool BGMPlayThrough::IsRunningSomewhereOtherThanBGMApp(const BGMAudioDevice& inBGMDevice) { auto type = inBGMDevice.GetPropertyData_CFType(kBGMRunningSomewhereOtherThanBGMAppAddress); return type && CFBooleanGetValue(static_cast(type)); } #pragma mark IOProcs // Note that the IOProcs will very likely not run on the same thread and that they intentionally // only lock mutexes around their use of mBuffer. // static OSStatus BGMPlayThrough::InputDeviceIOProc(AudioObjectID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* __nullable inClientData) { #pragma unused (inDevice, inNow, outOutputData, inOutputTime) // refCon (reference context) is the instance that created the IOProc BGMPlayThrough* const refCon = static_cast(inClientData); IOState state; UpdateIOProcState("InputDeviceIOProc", refCon->mRTLogger, refCon->mInputDeviceIOProcState, refCon->mInputDeviceIOProcID, refCon->mInputDevice, state); if(state == IOState::Stopped || state == IOState::Stopping) { // Return early, since we just asked to stop. (Or something really weird is going on.) return noErr; } BGMAssert(state == IOState::Running, "BGMPlayThrough::InputDeviceIOProc: Unexpected state"); if(refCon->mFirstInputSampleTime == -1) { refCon->mFirstInputSampleTime = inInputTime->mSampleTime; } UInt32 framesToStore = inInputData->mBuffers[0].mDataByteSize / (SizeOf32(Float32) * 2); // See the comments in OutputDeviceIOProc where it locks mBufferOutputMutex. CAMutex::Tryer tryer(refCon->mBufferInputMutex); // Disable a warning about accessing mBuffer without holding both mBufferInputMutex and // mBufferOutputMutex. Explained further in OutputDeviceIOProc. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wthread-safety" if(tryer.HasLock() && refCon->mBuffer) { CARingBufferError err = refCon->mBuffer->Store(inInputData, framesToStore, static_cast( inInputTime->mSampleTime)); #pragma clang diagnostic pop refCon->mRTLogger.LogIfRingBufferError_Store(err); refCon->mLastInputSampleTime = inInputTime->mSampleTime; } else { refCon->mRTLogger.LogRingBufferUnavailable("InputDeviceIOProc", tryer.HasLock()); } return noErr; } // static OSStatus BGMPlayThrough::OutputDeviceIOProc(AudioObjectID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* __nullable inClientData) { #pragma unused (inDevice, inNow, inInputData, inInputTime) // refCon (reference context) is the instance that created the IOProc BGMPlayThrough* const refCon = static_cast(inClientData); IOState state; const bool didChangeState = UpdateIOProcState("OutputDeviceIOProc", refCon->mRTLogger, refCon->mOutputDeviceIOProcState, refCon->mOutputDeviceIOProcID, refCon->mOutputDevice, state); if(state == IOState::Stopped || state == IOState::Stopping) { // Return early, since we just asked to stop. (Or something really weird is going on.) FillWithSilence(outOutputData); return noErr; } BGMAssert(state == IOState::Running, "BGMPlayThrough::OutputDeviceIOProc: Unexpected state"); if(didChangeState) { // We just changed state from Starting to Running, which means this is the first time this IOProc // has been called since the output device finished starting up, so now we can wake any threads // waiting in WaitForOutputDeviceToStart. BGMAssert(refCon->mLastOutputSampleTime == -1, "BGMPlayThrough::OutputDeviceIOProc: mLastOutputSampleTime not reset"); refCon->ReleaseThreadsWaitingForOutputToStart(); } if(refCon->mLastInputSampleTime == -1) { // Return early, since we don't have any data to output yet. FillWithSilence(outOutputData); return noErr; } // If this is the first time this IOProc has been called since starting playthrough... if(refCon->mLastOutputSampleTime == -1) { // Calculate the number of frames between the read and write heads refCon->mInToOutSampleOffset = inOutputTime->mSampleTime - refCon->mLastInputSampleTime; // Log if we dropped frames refCon->mRTLogger.LogIfDroppedFrames(refCon->mFirstInputSampleTime, refCon->mLastInputSampleTime); } CARingBuffer::SampleTime readHeadSampleTime = static_cast(inOutputTime->mSampleTime - refCon->mInToOutSampleOffset); CARingBuffer::SampleTime lastInputSampleTime = static_cast(refCon->mLastInputSampleTime); UInt32 framesToOutput = outOutputData->mBuffers[0].mDataByteSize / (SizeOf32(Float32) * 2); // When the input and output devices are set, during start up or because the user changed the // output device, this class (re)allocates the ring buffer (mBuffer). We try to take this // lock before accessing the buffer to make sure it's allocated. // // If we don't get the lock, another thread must be allocating or deallocating it, so we just // give up. We can't avoid audio glitches while changing devices anyway. This class tries to // make sure the IOProcs aren't running when it allocates the buffer, but it can't guarantee // that. // // Note that this is only realtime safe because we only try to lock the mutex. If another // thread has the mutex, it will be a non-realtime thread, so we can't wait for it. CAMutex::Tryer tryer(refCon->mBufferOutputMutex); // Disable a warning about accessing mBuffer without holding both mBufferInputMutex and // mBufferOutputMutex. The input IOProc always writes ahead of where the output IOProc will read // in a given IO cycle, so it's safe for them to read and write at the same time. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wthread-safety" if(tryer.HasLock() && refCon->mBuffer) { // Very occasionally (at least for me) our read head gets ahead of input, i.e. we haven't // received any new input since this IOProc was last called, and we have to recalculate its // position. I figure this might be caused by clock drift but I'm really not sure. It also // happens if the input or output sample times are restarted from zero. // // We also recalculate the offset if the read head is outside of the ring buffer. This // happens for example when you plug in or unplug headphones, which causes the output sample // times to be restarted from zero. // // The vast majority of the time, just using lastInputSampleTime as the read head time // instead of the one we calculate would work fine (and would also account for the above). SInt64 bufferStartTime, bufferEndTime; CARingBufferError err = refCon->mBuffer->GetTimeBounds(bufferStartTime, bufferEndTime); bool outOfBounds = false; if(err == kCARingBufferError_OK) { outOfBounds = (readHeadSampleTime < bufferStartTime) || (readHeadSampleTime - framesToOutput > bufferEndTime); } if(lastInputSampleTime < readHeadSampleTime || outOfBounds) { refCon->mRTLogger.LogNoSamplesReady(lastInputSampleTime, readHeadSampleTime, refCon->mInToOutSampleOffset); // Recalculate the in-to-out offset and read head. refCon->mInToOutSampleOffset = inOutputTime->mSampleTime - lastInputSampleTime; readHeadSampleTime = static_cast( inOutputTime->mSampleTime - refCon->mInToOutSampleOffset); } // Copy the frames from the ring buffer. err = refCon->mBuffer->Fetch(outOutputData, framesToOutput, readHeadSampleTime); refCon->mRTLogger.LogIfRingBufferError_Fetch(err); if(err != kCARingBufferError_OK) { FillWithSilence(outOutputData); } } else { refCon->mRTLogger.LogRingBufferUnavailable("OutputDeviceIOProc", tryer.HasLock()); FillWithSilence(outOutputData); } #pragma clang diagnostic pop refCon->mLastOutputSampleTime = inOutputTime->mSampleTime; return noErr; } // static inline void BGMPlayThrough::FillWithSilence(AudioBufferList* ioBuffer) { for(UInt32 i = 0; i < ioBuffer->mNumberBuffers; i++) { memset(ioBuffer->mBuffers[i].mData, 0, ioBuffer->mBuffers[i].mDataByteSize); } } // static bool BGMPlayThrough::UpdateIOProcState(const char* inCallerName, BGMPlayThroughRTLogger& inRTLogger, std::atomic& inState, AudioDeviceIOProcID __nullable inIOProcID, BGMAudioDevice& inDevice, IOState& outNewState) { BGMAssert(inIOProcID != nullptr, "BGMPlayThrough::UpdateIOProcState: !inIOProcID"); // Change this IOProc's state to Running if this is the first time it's been called since we // started playthrough. // // compare_exchange_strong will return true iff it changed inState from Starting to Running. // Otherwise it will set prevState to the current value of inState. // // TODO: We probably don't actually need memory_order_seq_cst (the default). Would it be worth // changing? Might be worth checking for the other atomics/barriers in this class, too. IOState prevState = IOState::Starting; bool didChangeState = inState.compare_exchange_strong(prevState, IOState::Running); if(didChangeState) { BGMAssert(prevState == IOState::Starting, "BGMPlayThrough::UpdateIOProcState: ?!"); outNewState = IOState::Running; } else { // Return the current value of inState to the caller. outNewState = prevState; if(outNewState != IOState::Running) { // The IOProc isn't Starting or Running, so it must be Stopping. That is, it's been // told to stop itself. BGMAssert(outNewState == IOState::Stopping, "BGMPlayThrough::UpdateIOProcState: Unexpected state: %d", outNewState); bool stoppedSuccessfully = false; try { inDevice.StopIOProc(inIOProcID); // StopIOProc didn't throw, so the IOProc won't be called again until the next // time playthrough is started. stoppedSuccessfully = true; } catch(CAException e) { inRTLogger.LogExceptionStoppingIOProc(inCallerName, e.GetError()); } catch(...) { inRTLogger.LogExceptionStoppingIOProc(inCallerName); } if(stoppedSuccessfully) { // Change inState to Stopped. // // If inState has been changed since we last read it, we don't know if we called // StopIOProc before or after the thread that changed it called StartIOProc (if it // did). However, inState is only changed here (in the IOProc), in Start and in // Stop. // // Stop won't return until the IOProc has changed inState to Stopped, unless it // times out, so Stop should still be waiting. And since Start and Stop are // mutually exclusive, this should be safe. // // But if Stop has timed out and inState has changed, we leave it in its new // state (unless there's some ABA problem thing happening), which I suspect is // the safest option. didChangeState = inState.compare_exchange_strong(outNewState, IOState::Stopped); if(didChangeState) { outNewState = IOState::Stopped; } else { inRTLogger.LogUnexpectedIOStateAfterStopping(inCallerName, static_cast(outNewState)); } } } } return didChangeState; } ================================================ FILE: BGMApp/BGMApp/BGMPlayThrough.h ================================================ // 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 . // // BGMPlayThrough.h // BGMApp // // Copyright © 2016, 2017, 2020 Kyle Neideck // // Reads audio from an input device and immediately writes it to an output device. We currently use this class with the input // device always set to BGMDevice and the output device set to the one selected in the preferences menu. // // Apple's CAPlayThrough sample code (https://developer.apple.com/library/mac/samplecode/CAPlayThrough/Introduction/Intro.html) // has a similar class, but I couldn't get it fast enough to use here. Soundflower also has a similar class // (https://github.com/mattingalls/Soundflower/blob/master/SoundflowerBed/AudioThruEngine.h) that seems to be based on Apple // sample code from 2004. This class's main addition is pausing playthrough when idle to save CPU. // // Playing audio with this class uses more CPU, mostly in the coreaudiod process, than playing audio normally because we need // an input IOProc as well as an output one, and BGMDriver is running in addition to the output device's driver. For me, it // usually adds around 1-2% (as a percentage of total usage -- it doesn't seem to be relative to the CPU used when playing // audio normally). // // This class will hopefully not be needed after CoreAudio's aggregate devices get support for controls, which is planned for // a future release. // #ifndef BGMApp__BGMPlayThrough #define BGMApp__BGMPlayThrough // Local Includes #include "BGMAudioDevice.h" #include "BGMPlayThroughRTLogger.h" // PublicUtility Includes #include "CAMutex.h" #include "CARingBuffer.h" #include "BGMThreadSafetyAnalysis.h" // STL Includes #include #include #include // System Includes #include #pragma clang assume_nonnull begin class BGMPlayThrough { public: // Error codes static const OSStatus kDeviceNotStarting = 100; public: BGMPlayThrough(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice); ~BGMPlayThrough(); // Disallow copying BGMPlayThrough(const BGMPlayThrough&) = delete; BGMPlayThrough& operator=(const BGMPlayThrough&) = delete; #ifdef __OBJC__ // Only intended as a convenience (hack) for Objective-C instance vars. Call // SetDevices to initialise the instance before using it. BGMPlayThrough() { } #endif private: /*! @throws CAException */ void Init(BGMAudioDevice inInputDevice, BGMAudioDevice inOutputDevice) REQUIRES(mStateMutex); public: /*! @throws CAException */ void Activate(); /*! @throws CAException */ void Deactivate(); private: void AllocateBuffer() REQUIRES(mStateMutex); void DeallocateBuffer(); /*! @throws CAException */ void CreateIOProcIDs(); /*! @throws CAException */ void DestroyIOProcIDs(); /*! @return True if both IOProcs are stopped. @nonthreadsafe */ bool CheckIOProcsAreStopped() const noexcept REQUIRES(mStateMutex); public: /*! Pass null for either param to only change one of the devices. @throws CAException */ void SetDevices(const BGMAudioDevice* __nullable inInputDevice, const BGMAudioDevice* __nullable inOutputDevice); /*! @throws CAException */ void Start(); // Blocks until the output device has started our IOProc. Returns one of the error constants // from AudioHardwareBase.h (e.g. kAudioHardwareNoError). OSStatus WaitForOutputDeviceToStart() noexcept; private: /*! Real-time safe. */ void ReleaseThreadsWaitingForOutputToStart(); public: OSStatus Stop(); void StopIfIdle(); private: static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData); static void HandleBGMDeviceIsRunning(BGMPlayThrough* refCon); static void HandleBGMDeviceIsRunningSomewhereOtherThanBGMApp(BGMPlayThrough* refCon); static bool IsRunningSomewhereOtherThanBGMApp(const BGMAudioDevice& inBGMDevice); static OSStatus InputDeviceIOProc(AudioObjectID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* __nullable inClientData); static OSStatus OutputDeviceIOProc(AudioObjectID inDevice, const AudioTimeStamp* inNow, const AudioBufferList* inInputData, const AudioTimeStamp* inInputTime, AudioBufferList* outOutputData, const AudioTimeStamp* inOutputTime, void* __nullable inClientData); /*! Fills the given ABL with zeroes to make it silent. */ static inline void FillWithSilence(AudioBufferList* ioBuffer); // The state of an IOProc. Used by the IOProc to tell other threads when it's finished starting. Used by other // threads to tell the IOProc to stop itself. (Probably used for other things as well.) enum class IOState { Stopped, Starting, Running, Stopping }; // The IOProcs call this to update their IOState member. Also stops the IOProc if its state has been set to Stopping. // Returns true if it changes the state. static bool UpdateIOProcState(const char* inCallerName, BGMPlayThroughRTLogger& inRTLogger, std::atomic& inState, AudioDeviceIOProcID __nullable inIOProcID, BGMAudioDevice& inDevice, IOState& outNewState); private: std::unique_ptr mBuffer PT_GUARDED_BY(mBufferInputMutex) PT_GUARDED_BY(mBufferOutputMutex) { nullptr }; AudioDeviceIOProcID __nullable mInputDeviceIOProcID { nullptr }; AudioDeviceIOProcID __nullable mOutputDeviceIOProcID { nullptr }; BGMAudioDevice mInputDevice { kAudioObjectUnknown }; BGMAudioDevice mOutputDevice { kAudioObjectUnknown }; // mStateMutex is the general purpose mutex. mBufferInputMutex and mBufferOutputMutex are // just used to make sure mBuffer, the ring buffer, is allocated when the IOProcs access it. See // the comments in the IOProcs for details. // // If a thread might lock more than one of these mutexes, it *must* take them in this order: // 1. mStateMutex // 2. mBufferInputMutex // 3. mBufferOutputMutex // // The ACQUIRED_BEFORE annotations don't do anything yet. From clang's docs: "ACQUIRED_BEFORE(…) // and ACQUIRED_AFTER(…) are currently unimplemented. To be fixed in a future update." After // they've fixed that, the compiler will enforce the ordering statically. // // TODO: We can't use std::shared_lock because we're still on C++11, but we could use std::lock // to help ensure the locks are always taken in the right order. // TODO: It would be better to have a separate class for the buffer and its mutexes. CAMutex mStateMutex ACQUIRED_BEFORE(mBufferInputMutex) ACQUIRED_BEFORE(mBufferOutputMutex) { "Playthrough state" }; CAMutex mBufferInputMutex ACQUIRED_BEFORE(mBufferOutputMutex) { "Playthrough ring buffer input" }; CAMutex mBufferOutputMutex { "Playthrough ring buffer output" }; // Signalled when the output IOProc runs. We use it to tell BGMDriver when the output device is ready to receive audio data. semaphore_t mOutputDeviceIOProcSemaphore { SEMAPHORE_NULL }; bool mActive = false; bool mPlayingThrough = false; UInt64 mLastNotifiedIOStoppedOnBGMDevice { 0 }; std::atomic mInputDeviceIOProcState { IOState::Stopped }; std::atomic mOutputDeviceIOProcState { IOState::Stopped }; // For debug logging. UInt64 mToldOutputDeviceToStartAt { 0 }; // IOProc vars. (Should only be used inside IOProcs.) // The earliest/latest sample times seen by the IOProcs since starting playthrough. -1 for unset. Float64 mFirstInputSampleTime = -1; Float64 mLastInputSampleTime = -1; Float64 mLastOutputSampleTime = -1; // Subtract this from the output time to get the input time. Float64 mInToOutSampleOffset { 0.0 }; BGMPlayThroughRTLogger mRTLogger; }; #pragma clang assume_nonnull end #endif /* BGMApp__BGMPlayThrough */ ================================================ FILE: BGMApp/BGMApp/BGMPlayThroughRTLogger.cpp ================================================ // 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 . // // BGMPlayThroughRTLogger.cpp // BGMApp // // Copyright © 2020 Kyle Neideck // // Self Include #include "BGMPlayThroughRTLogger.h" // Local Includes #include "BGM_Utils.h" // PublicUtility Includes #include "CADebugMacros.h" // STL Includes #include // System Includes #include #include #include #include #pragma clang assume_nonnull begin // Track the number of messages logged when built for the unit tests. #if BGM_UnitTest #define LogSync_Debug(inFormat, ...) do { \ mNumDebugMessagesLogged++; \ DebugMsg(inFormat, ## __VA_ARGS__); \ } while (0) #else #define LogSync_Debug(inFormat, ...) DebugMsg(inFormat, ## __VA_ARGS__) #endif #pragma mark Construction/Destruction BGMPlayThroughRTLogger::BGMPlayThroughRTLogger() { // Create the semaphore we use to wake up the logging thread when it has messages to log. mWakeUpLoggingThreadSemaphore = CreateSemaphore(); // Create the logging thread last because it starts immediately and expects the other member // variables to be initialised. mLoggingThread = std::thread(&BGMPlayThroughRTLogger::LoggingThreadEntry, this); } // static semaphore_t BGMPlayThroughRTLogger::CreateSemaphore() { // TODO: Make a BGMMachSemaphore class to reduce some of this repetitive semaphore code. // Create the semaphore. semaphore_t semaphore; kern_return_t error = semaphore_create(mach_task_self(), &semaphore, SYNC_POLICY_FIFO, 0); // Check the error code. BGM_Utils::ThrowIfMachError("BGMPlayThroughRTLogger::CreateSemaphore", "semaphore_create", error); ThrowIf(semaphore == SEMAPHORE_NULL, CAException(kAudioHardwareUnspecifiedError), "BGMPlayThroughRTLogger::CreateSemaphore: Failed to create semaphore"); return semaphore; } BGMPlayThroughRTLogger::~BGMPlayThroughRTLogger() { // Stop the logging thread. mLoggingThreadShouldExit = true; kern_return_t error = semaphore_signal(mWakeUpLoggingThreadSemaphore); BGM_Utils::LogIfMachError("BGMPlayThroughRTLogger::~BGMPlayThroughRTLogger", "semaphore_signal", error); if(error == KERN_SUCCESS) { // Wait for it to stop. mLoggingThread.join(); // Destroy the semaphore. error = semaphore_destroy(mach_task_self(), mWakeUpLoggingThreadSemaphore); BGM_Utils::LogIfMachError("BGMPlayThroughRTLogger::~BGMPlayThroughRTLogger", "semaphore_destroy", error); } else { // If we couldn't tell it to wake up, it's not safe to wait for it to stop or to destroy the // semaphore. We have to detach it so its destructor doesn't cause a crash. mLoggingThread.detach(); } } #pragma mark Log Messages void BGMPlayThroughRTLogger::LogReleasingWaitingThreads() { if(!BGMDebugLoggingIsEnabled()) { return; } if(!mLogReleasingWaitingThreadsMsg.is_lock_free()) { // Modifying mLogReleasingWaitingThreadsMsg might cause the thread to lock a mutex that // isn't safe to lock on a realtime thread, so just give up. return; } // Set the flag that tells the logging thread to log the message. mLogReleasingWaitingThreadsMsg = true; // Wake the logging thread so it can log the message. WakeLoggingThread(); } void BGMPlayThroughRTLogger::LogIfMachError_ReleaseWaitingThreadsSignal(mach_error_t inError) { if(inError == KERN_SUCCESS) { // No error. return; } if(!mReleaseWaitingThreadsSignalError.is_lock_free()) { // Modifying mReleaseWaitingThreadsSignalError might cause the thread to lock a mutex that // isn't safe to lock on a realtime thread, so just give up. return; } mReleaseWaitingThreadsSignalError = inError; WakeLoggingThread(); } void BGMPlayThroughRTLogger::LogIfDroppedFrames(Float64 inFirstInputSampleTime, Float64 inLastInputSampleTime) { if(inFirstInputSampleTime == inLastInputSampleTime || !BGMDebugLoggingIsEnabled()) { // Either we didn't drop any initial frames or we don't need to log a message about it. return; } LogAsync(mDroppedFrames, [&]() { // Store the data to include in the log message. mDroppedFrames.firstInputSampleTime = inFirstInputSampleTime; mDroppedFrames.lastInputSampleTime = inLastInputSampleTime; }); } void BGMPlayThroughRTLogger::LogNoSamplesReady(CARingBuffer::SampleTime inLastInputSampleTime, CARingBuffer::SampleTime inReadHeadSampleTime, Float64 inInToOutSampleOffset) { if(!BGMDebugLoggingIsEnabled()) { return; } LogAsync(mNoSamplesReady, [&]() { // Store the data to include in the log message. mNoSamplesReady.lastInputSampleTime = inLastInputSampleTime; mNoSamplesReady.readHeadSampleTime = inReadHeadSampleTime; mNoSamplesReady.inToOutSampleOffset = inInToOutSampleOffset; }); } void BGMPlayThroughRTLogger::LogExceptionStoppingIOProc(const char* inCallerName, OSStatus inError, bool inErrorKnown) { LogAsync(mExceptionStoppingIOProc, [&]() { // Store the data to include in the log message. mExceptionStoppingIOProc.callerName = inCallerName; mExceptionStoppingIOProc.error = inError; mExceptionStoppingIOProc.errorKnown = inErrorKnown; }); } void BGMPlayThroughRTLogger::LogUnexpectedIOStateAfterStopping(const char* inCallerName, int inIOState) { LogAsync(mUnexpectedIOStateAfterStopping, [&]() { // Store the data to include in the log message. mUnexpectedIOStateAfterStopping.callerName = inCallerName; mUnexpectedIOStateAfterStopping.ioState = inIOState; }); } void BGMPlayThroughRTLogger::LogRingBufferUnavailable(const char* inCallerName, bool inGotLock) { LogAsync(mRingBufferUnavailable, [&]() { // Store the data to include in the log message. mRingBufferUnavailable.callerName = inCallerName; mRingBufferUnavailable.gotLock = inGotLock; }); } void BGMPlayThroughRTLogger::LogIfRingBufferError(CARingBufferError inError, std::atomic& outError) { if(inError == kCARingBufferError_OK) { // No error. return; } if(!outError.is_lock_free()) { // Modifying outError might cause the thread to lock a mutex that isn't safe to lock on // a realtime thread, so just give up. return; } // Store the error. outError = inError; // Wake the logging thread so it can log the error. WakeLoggingThread(); } template void BGMPlayThroughRTLogger::LogAsync(T& inMessageData, F&& inStoreMessageData) { if(!inMessageData.shouldLogMessage.is_lock_free()) { // Modifying shouldLogMessage might cause the thread to lock a mutex that isn't safe to // lock on a realtime thread, so just give up. return; } if(inMessageData.shouldLogMessage) { // The logging thread could be reading inMessageData. return; } // Store the data to include in the log message. // // std::forward lets the compiler treat inStoreMessageData as an rvalue if the caller gave it as // an rvalue. No idea if that actually does anything. std::forward(inStoreMessageData)(); // shouldLogMessage is a std::atomic, so this store also makes sure that the non-atomic stores // in inStoreMessageData will be visible to the logger thread (since the default memory order is // memory_order_seq_cst). inMessageData.shouldLogMessage = true; WakeLoggingThread(); } void BGMPlayThroughRTLogger::LogSync_Warning(const char* inFormat, ...) { va_list args; va_start(args, inFormat); #if BGM_UnitTest mNumWarningMessagesLogged++; #endif vLogWarning(inFormat, args); va_end(args); } void BGMPlayThroughRTLogger::LogSync_Error(const char* inFormat, ...) { va_list args; va_start(args, inFormat); #if BGM_UnitTest mNumErrorMessagesLogged++; if(!mContinueOnErrorLogged) { vLogError(inFormat, args); } #else vLogError(inFormat, args); #endif va_end(args); } #pragma mark Logging Thread void BGMPlayThroughRTLogger::WakeLoggingThread() { kern_return_t error = semaphore_signal(mWakeUpLoggingThreadSemaphore); BGMAssert(error == KERN_SUCCESS, "semaphore_signal (%d)", error); // We can't do anything useful with the error in release builds. At least, not easily. (void)error; } void BGMPlayThroughRTLogger::LogMessages() { // Log the messages/errors from the realtime threads (if any). LogSync_ReleasingWaitingThreads(); LogSync_ReleaseWaitingThreadsSignalError(); LogSync_DroppedFrames(); LogSync_NoSamplesReady(); LogSync_ExceptionStoppingIOProc(); LogSync_UnexpectedIOStateAfterStopping(); LogSync_RingBufferUnavailable(); LogSync_RingBufferError(mRingBufferStoreError, "InputDeviceIOProc"); LogSync_RingBufferError(mRingBufferFetchError, "OutputDeviceIOProc"); } void BGMPlayThroughRTLogger::LogSync_ReleasingWaitingThreads() { if(mLogReleasingWaitingThreadsMsg) { LogSync_Debug("BGMPlayThrough::ReleaseThreadsWaitingForOutputToStart: " "Releasing waiting threads"); // Reset it. mLogReleasingWaitingThreadsMsg = false; } } void BGMPlayThroughRTLogger::LogSync_ReleaseWaitingThreadsSignalError() { if(mReleaseWaitingThreadsSignalError != KERN_SUCCESS) { BGM_Utils::LogIfMachError("BGMPlayThrough::ReleaseThreadsWaitingForOutputToStart", "semaphore_signal_all", mReleaseWaitingThreadsSignalError); // Reset it. mReleaseWaitingThreadsSignalError = KERN_SUCCESS; } } void BGMPlayThroughRTLogger::LogSync_DroppedFrames() { if(mDroppedFrames.shouldLogMessage) { LogSync_Debug("BGMPlayThrough::OutputDeviceIOProc: " "Dropped %f frames before output started. %s%f %s%f", (mDroppedFrames.lastInputSampleTime - mDroppedFrames.firstInputSampleTime), "mFirstInputSampleTime=", mDroppedFrames.firstInputSampleTime, "mLastInputSampleTime=", mDroppedFrames.lastInputSampleTime); mDroppedFrames.shouldLogMessage = false; } } void BGMPlayThroughRTLogger::LogSync_NoSamplesReady() { if(mNoSamplesReady.shouldLogMessage) { LogSync_Debug("BGMPlayThrough::OutputDeviceIOProc: " "No input samples ready at output sample time. %s%lld %s%lld %s%f", "lastInputSampleTime=", mNoSamplesReady.lastInputSampleTime, "readHeadSampleTime=", mNoSamplesReady.readHeadSampleTime, "mInToOutSampleOffset=", mNoSamplesReady.inToOutSampleOffset); mNoSamplesReady.shouldLogMessage = false; } } void BGMPlayThroughRTLogger::LogSync_ExceptionStoppingIOProc() { if(mExceptionStoppingIOProc.shouldLogMessage) { const char error4CC[5] = CA4CCToCString(mExceptionStoppingIOProc.error); LogSync_Error("BGMPlayThrough::UpdateIOProcState: " "Exception while stopping IOProc %s: %s (%d)", mExceptionStoppingIOProc.callerName, mExceptionStoppingIOProc.errorKnown ? error4CC : "unknown", mExceptionStoppingIOProc.error); mExceptionStoppingIOProc.shouldLogMessage = false; } } void BGMPlayThroughRTLogger::LogSync_UnexpectedIOStateAfterStopping() { if(mUnexpectedIOStateAfterStopping.shouldLogMessage) { LogSync_Warning("BGMPlayThrough::UpdateIOProcState: " "%s IO state changed since last read. state = %d", mUnexpectedIOStateAfterStopping.callerName, mUnexpectedIOStateAfterStopping.ioState); mUnexpectedIOStateAfterStopping.shouldLogMessage = false; } } void BGMPlayThroughRTLogger::LogSync_RingBufferUnavailable() { if(mRingBufferUnavailable.shouldLogMessage) { LogSync_Warning("BGMPlayThrough::%s: Ring buffer unavailable. %s", mRingBufferUnavailable.callerName, mRingBufferUnavailable.gotLock ? "No buffer currently allocated." : "Buffer locked for allocation/deallocation by another thread."); mRingBufferUnavailable.shouldLogMessage = false; } } void BGMPlayThroughRTLogger::LogSync_RingBufferError( std::atomic& ioRingBufferError, const char* inMethodName) { CARingBufferError error = ioRingBufferError; switch(error) { case kCARingBufferError_OK: // No error. return; case kCARingBufferError_CPUOverload: // kCARingBufferError_CPUOverload might not be our fault, so just log a warning. LogSync_Warning("BGMPlayThrough::%s: Ring buffer error: " "kCARingBufferError_CPUOverload (%d)", inMethodName, error); break; default: // Other types of CARingBuffer errors should never occur. This will crash debug builds. LogSync_Error("BGMPlayThrough::%s: Ring buffer error: %s (%d)", inMethodName, (error == kCARingBufferError_TooMuch ? "kCARingBufferError_TooMuch" : "unknown error"), error); break; }; // Reset it. ioRingBufferError = kCARingBufferError_OK; } // static void* __nullable BGMPlayThroughRTLogger::LoggingThreadEntry(BGMPlayThroughRTLogger* inRefCon) { DebugMsg("BGMPlayThroughRTLogger::IOProcLoggingThreadEntry: " "Starting the IOProc logging thread"); while(!inRefCon->mLoggingThreadShouldExit) { // Log the messages, if there are any to log. inRefCon->LogMessages(); // Wait until woken up. kern_return_t error = semaphore_wait(inRefCon->mWakeUpLoggingThreadSemaphore); BGM_Utils::LogIfMachError("BGMPlayThroughRTLogger::IOProcLoggingThreadEntry", "semaphore_wait", error); } DebugMsg("BGMPlayThroughRTLogger::IOProcLoggingThreadEntry: IOProc logging thread exiting"); return nullptr; } #if BGM_UnitTest #pragma mark Test Helpers bool BGMPlayThroughRTLogger::WaitUntilLoggerThreadIdle() { int msWaited = 0; while(mLogReleasingWaitingThreadsMsg || mReleaseWaitingThreadsSignalError != KERN_SUCCESS || mDroppedFrames.shouldLogMessage || mNoSamplesReady.shouldLogMessage || mUnexpectedIOStateAfterStopping.shouldLogMessage || mRingBufferUnavailable.shouldLogMessage || mExceptionStoppingIOProc.shouldLogMessage || mRingBufferStoreError != kCARingBufferError_OK || mRingBufferFetchError != kCARingBufferError_OK) { // Poll until the logger thread has nothing left to log. (Ideally we'd use a semaphore // instead of polling, but it isn't worth the effort at this point.) usleep(10 * 1000); msWaited += 10; // Time out after 5 seconds. if(msWaited > 5000) { return false; } } return true; } #endif /* BGM_UnitTest */ #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMPlayThroughRTLogger.h ================================================ // 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 . // // BGMPlayThroughRTLogger.h // BGMApp // // Copyright © 2020 Kyle Neideck // // A real-time safe logger for BGMPlayThrough. The messages are logged asynchronously by a // non-realtime thread. // // For the sake of simplicity, this class is very closely coupled with BGMPlayThrough and its // methods make assumptions about where they will be called. Also, if the same logging method is // called multiple times before the logging thread next checks for messages, it will only log the // message for one of those calls and ignore the others. // // This class's methods are real-time safe in that they return in a bounded amount of time and we // think they're probably fast enough that the callers won't miss their deadlines, but we don't try // to guarantee it. Some of them should only be called in unusual cases where it's worth increasing // the risk of a thread missing its deadline. // #ifndef BGMApp__BGMPlayThroughRTLogger #define BGMApp__BGMPlayThroughRTLogger // PublicUtility Includes #include "CARingBuffer.h" // STL Includes #include // System Includes #include #include #pragma clang assume_nonnull begin class BGMPlayThroughRTLogger { #pragma mark Construction/Destruction public: BGMPlayThroughRTLogger(); ~BGMPlayThroughRTLogger(); BGMPlayThroughRTLogger(const BGMPlayThroughRTLogger&) = delete; BGMPlayThroughRTLogger& operator=( const BGMPlayThroughRTLogger&) = delete; private: static semaphore_t CreateSemaphore(); #pragma mark Log Messages public: /*! For BGMPlayThrough::ReleaseThreadsWaitingForOutputToStart. */ void LogReleasingWaitingThreads(); /*! For BGMPlayThrough::ReleaseThreadsWaitingForOutputToStart. */ void LogIfMachError_ReleaseWaitingThreadsSignal(mach_error_t inError); /*! For BGMPlayThrough::OutputDeviceIOProc. Not thread-safe. */ void LogIfDroppedFrames(Float64 inFirstInputSampleTime, Float64 inLastInputSampleTime); /*! For BGMPlayThrough::OutputDeviceIOProc. Not thread-safe. */ void LogNoSamplesReady(CARingBuffer::SampleTime inLastInputSampleTime, CARingBuffer::SampleTime inReadHeadSampleTime, Float64 inInToOutSampleOffset); /*! For BGMPlayThrough::UpdateIOProcState. Not thread-safe. */ void LogExceptionStoppingIOProc(const char* inCallerName) { LogExceptionStoppingIOProc(inCallerName, noErr, false); } /*! For BGMPlayThrough::UpdateIOProcState. Not thread-safe. */ void LogExceptionStoppingIOProc(const char* inCallerName, OSStatus inError) { LogExceptionStoppingIOProc(inCallerName, inError, true); } private: void LogExceptionStoppingIOProc(const char* inCallerName, OSStatus inError, bool inErrorKnown); public: /*! For BGMPlayThrough::UpdateIOProcState. Not thread-safe. */ void LogUnexpectedIOStateAfterStopping(const char* inCallerName, int inIOState); /*! For BGMPlayThrough::InputDeviceIOProc and BGMPlayThrough::OutputDeviceIOProc. */ void LogRingBufferUnavailable(const char* inCallerName, bool inGotLock); /*! For BGMPlayThrough::OutputDeviceIOProc. */ void LogIfRingBufferError_Fetch(CARingBufferError inError) { LogIfRingBufferError(inError, mRingBufferFetchError); } /*! For BGMPlayThrough::InputDeviceIOProc. */ void LogIfRingBufferError_Store(CARingBufferError inError) { LogIfRingBufferError(inError, mRingBufferStoreError); } private: void LogIfRingBufferError(CARingBufferError inError, std::atomic& outError); template void LogAsync(T& inMessageData, F&& inStoreMessageData); // Wrapper methods used to mock out the logging for unit tests. void LogSync_Warning(const char* inFormat, ...) __printflike(2, 3); void LogSync_Error(const char* inFormat, ...) __printflike(2, 3); #pragma mark Logging Thread private: void WakeLoggingThread(); void LogMessages(); void LogSync_ReleasingWaitingThreads(); void LogSync_ReleaseWaitingThreadsSignalError(); void LogSync_DroppedFrames(); void LogSync_NoSamplesReady(); void LogSync_ExceptionStoppingIOProc(); void LogSync_UnexpectedIOStateAfterStopping(); void LogSync_RingBufferUnavailable(); void LogSync_RingBufferError( std::atomic& ioRingBufferError, const char* inMethodName); // The entry point of the logging thread (mLoggingThread). static void* __nullable LoggingThreadEntry(BGMPlayThroughRTLogger* inRefCon); #if BGM_UnitTest #pragma mark Test Helpers public: /*! * @return True if the logger thread finished logging the requested messages. False if it still * had messages to log after 5 seconds. */ bool WaitUntilLoggerThreadIdle(); #endif /* BGM_UnitTest */ private: // For BGMPlayThrough::ReleaseThreadsWaitingForOutputToStart std::atomic mLogReleasingWaitingThreadsMsg { false }; std::atomic mReleaseWaitingThreadsSignalError { KERN_SUCCESS }; // For BGMPlayThrough::InputDeviceIOProc and BGMPlayThrough::OutputDeviceIOProc struct { Float64 firstInputSampleTime; Float64 lastInputSampleTime; std::atomic shouldLogMessage { false }; } mDroppedFrames; struct { CARingBuffer::SampleTime lastInputSampleTime; CARingBuffer::SampleTime readHeadSampleTime; Float64 inToOutSampleOffset; std::atomic shouldLogMessage { false }; } mNoSamplesReady; struct { const char* callerName; bool gotLock; std::atomic shouldLogMessage { false }; } mRingBufferUnavailable; // For BGMPlayThrough::UpdateIOProcState struct { const char* callerName; int ioState; std::atomic shouldLogMessage { false }; } mUnexpectedIOStateAfterStopping; struct { const char* callerName; OSStatus error; bool errorKnown; // If false, we didn't get an error code from the exception. std::atomic shouldLogMessage { false }; } mExceptionStoppingIOProc; // For BGMPlayThrough::OutputDeviceIOProc std::atomic mRingBufferStoreError { kCARingBufferError_OK }; // For BGMPlayThrough::InputDeviceIOProc. std::atomic mRingBufferFetchError { kCARingBufferError_OK }; // Signalled to wake up the mLoggingThread when it has messages to log. semaphore_t mWakeUpLoggingThreadSemaphore; std::atomic mLoggingThreadShouldExit { false }; // The thread that actually logs the messages. std::thread mLoggingThread; #if BGM_UnitTest public: // Tests normally crash (abort) if LogError is called. This flag lets us test the code that // would otherwise call LogError. bool mContinueOnErrorLogged { false }; int mNumDebugMessagesLogged { 0 }; int mNumWarningMessagesLogged { 0 }; int mNumErrorMessagesLogged { 0 }; #endif /* BGM_UnitTest */ }; #pragma clang assume_nonnull end #endif /* BGMApp__BGMPlayThroughRTLogger */ ================================================ FILE: BGMApp/BGMApp/BGMPreferredOutputDevices.h ================================================ // 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 . // // BGMPreferredOutputDevices.h // BGMApp // // Copyright © 2018 Kyle Neideck // // Tries to change BGMApp's output device when the user plugs in or unplugs an audio device, in the // same way macOS would change its default device if Background Music wasn't running. // // For example, if you plug in some USB headphones, make them your default device and then unplug // them, macOS will change its default device to the previous default device. Then, if you plug // them back in, macOS will make them the default device again. // // This class isn't always able to figure out what macOS would do, in which case it leaves BGMApp's // output device as it is. // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMUserDefaults.h" // System Includes #import #import #pragma clang assume_nonnull begin @interface BGMPreferredOutputDevices : NSObject // Starts responding to device connections/disconnections immediately. Stops if/when the instance is // deallocated. - (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices userDefaults:(BGMUserDefaults*)userDefaults; // Returns the most-preferred device that's currently connected. If no preferred devices are // connected, returns the current output device. If the current output device has been disconnected, // returns an arbitrary device. // // If none of the connected devices can be used as the output device, or if it can't find a device // to use because the HAL returned errors when queried, returns kAudioObjectUnknown. - (AudioObjectID) findPreferredDevice; - (void) userChangedOutputDeviceTo:(AudioObjectID)device; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMPreferredOutputDevices.mm ================================================ // 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 . // // BGMPreferredOutputDevices.m // BGMApp // // Copyright © 2018 Kyle Neideck // // Self Include #import "BGMPreferredOutputDevices.h" // Local Includes #import "BGM_Types.h" #import "BGM_Utils.h" #import "BGMAudioDevice.h" // PublicUtility Includes #import "CAAutoDisposer.h" #import "CAHALAudioSystemObject.h" #import "CAPropertyAddress.h" #pragma clang assume_nonnull begin // The Plist file CoreAudio stores the preferred devices list in. It isn't part of the public API, // but if this class fails to read/parse it, BGMApp will continue without it. NSString* const kAudioSystemSettingsPlist = @"/Library/Preferences/Audio/com.apple.audio.SystemSettings.plist"; @implementation BGMPreferredOutputDevices { NSRecursiveLock* _stateLock; // Used to change BGMApp's output device. BGMAudioDeviceManager* _devices; // User settings and data. BGMUserDefaults* _userDefaults; // The UIDs of the preferred devices, in order of preference. The most-preferred device is at // index 0. This list is derived from several sources. NSArray* _preferredDeviceUIDs; // Called when a device is connected or disconnected. AudioObjectPropertyListenerBlock _deviceListListener; } - (instancetype) initWithDevices:(BGMAudioDeviceManager*)devices userDefaults:(BGMUserDefaults*)userDefaults { if ((self = [super init])) { _stateLock = [NSRecursiveLock new]; _devices = devices; _userDefaults = userDefaults; _preferredDeviceUIDs = [self readPreferredDevices]; DebugMsg("BGMPreferredOutputDevices::initWithDevices: Preferred devices: %s", _preferredDeviceUIDs.debugDescription.UTF8String); [self listenForDevicesAddedOrRemoved]; } return self; } - (void) dealloc { @try { [_stateLock lock]; // Tell CoreAudio not to call the listener block anymore. CAHALAudioSystemObject().RemovePropertyListenerBlock( CAPropertyAddress(kAudioHardwarePropertyDevices), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), _deviceListListener); } @finally { [_stateLock unlock]; } } // Reads the preferred devices list from CoreAudio's Plist file. Uses BGMApp's stored list to fill // in the blanks, if necessary. - (NSArray*) readPreferredDevices { // Read the Plist file into a dictionary. // // TODO: If this file doesn't exist, try paths used by older versions of macOS. (If there are // any, that is. I haven't checked.) NSURL* url = [NSURL fileURLWithPath:kAudioSystemSettingsPlist]; NSError* error = nil; NSData* data = [NSData dataWithContentsOfURL:url options:0 error:&error]; NSDictionary* settings = nil; if (data && !error) { settings = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:nil error:&error]; } // Default to a list with just the systemwide default device (or an empty list if that fails) if // we can't read the preferred devices from the Plist because preferredDeviceUIDsFrom will use // BGMApp's stored preferred devices to fill in the rest optimistically. This doesn't help us // tell when to switch to a newly connected device, but it should improve our chances of // switching to the best device if the current output device is disconnected. NSArray* _Nonnull preferredOutputDeviceInfos = @[]; // If we can't read the Plist, we only know that the current systemwide default device is the // most-preferred device that's currently connected. // // TODO: If we are able to read the Plist, check that the systemwide default device is the // most-preferred device in the list from the Plist that's also connected. If it isn't, // the format of the Plist has probably changed, so we should ignore its data and log a // warning. BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { BGMAudioDevice defaultDevice = CAHALAudioSystemObject().GetDefaultAudioDevice(false, false); NSString* __nullable defaultDeviceUID = (__bridge_transfer NSString* __nullable)defaultDevice.CopyDeviceUID(); if (defaultDeviceUID) { preferredOutputDeviceInfos = @[ @{ @"uid": BGMNN(defaultDeviceUID) } ]; } }); if (error || !data || !settings) { // The Plist file either doesn't exist or we weren't able to parse it. LogWarning("BGMPreferredOutputDevices::readPreferredDevices: Couldn't read %s. " "(data = %s, settings = %s) Error: %s", kAudioSystemSettingsPlist.UTF8String, data.debugDescription.UTF8String, settings.debugDescription.UTF8String, error.debugDescription.UTF8String); } else if (!settings[@"preferred devices"]) { // The Plist doesn't include the lists of preferred devices (for input, output and system // output). LogWarning("BGMPreferredOutputDevices::readPreferredDevices: No preferred devices in %s", settings.debugDescription.UTF8String); } else if (!settings[@"preferred devices"][@"output"]) { // The Plist doesn't include the list of preferred output devices. LogWarning("BGMPreferredOutputDevices::readPreferredDevices: " "No preferred output devices in %s", settings.debugDescription.UTF8String); } else { // Copy the preferred devices out of the Plist. preferredOutputDeviceInfos = BGMNN(settings[@"preferred devices"][@"output"]); } return [self preferredDeviceUIDsFrom:preferredOutputDeviceInfos storedPreferredDeviceUIDs:_userDefaults.preferredDeviceUIDs]; } - (NSArray*) preferredDeviceUIDsFrom:(NSArray*)deviceInfos storedPreferredDeviceUIDs:(NSArray*)storedUIDs { NSArray* deviceUIDs = @[]; int storedPreferredDeviceIdx = 0; for (NSDictionary* deviceInfo in deviceInfos) { // Check the Plist actually has a UID for this device. if (![deviceInfo[@"uid"] isKindOfClass:NSString.class]) { LogWarning("BGMPreferredOutputDevices::preferredDeviceUIDsFrom: No UID in %s", deviceInfo.debugDescription.UTF8String); continue; } NSString* uid = deviceInfo[@"uid"]; if ([uid isEqualToString:@kBGMDeviceUID] || [uid isEqualToString:@kBGMDeviceUID_UISounds] || [uid isEqualToString:@kBGMNullDeviceUID]) { // This is one of the Background Music devices, so look for a preferred device saved // from a previous run of BGMApp and add it instead. // // BGMApp has to set BGMDevice, and often also the Null Device for a short time, as the // systemwide default audio device, which makes CoreAudio put them in its Plist. And // since the Plist is limited to three devices, it only gives us one or two usable ones. // Ideally, CoreAudio just wouldn't add our devices to its list, but I don't think we // can prevent that. And we can't be sure that editing its Plist file ourselves would be // safe. // // TODO: This doesn't work if the user has made BGMDevice the systemwide default device // themselves since the last time they closed BGMApp. We might be able to fix that // by having a background process watch the Plist for changes while BGMApp is // closed or something like that, but I doubt there's a nice or simple solution. deviceUIDs = [self addNextStoredPreferredDevice:&storedPreferredDeviceIdx preferredDeviceUIDs:deviceUIDs storedPreferredDeviceUIDs:storedUIDs]; } else if (![deviceUIDs containsObject:uid]) { // Add this preferred device's UID to the list. deviceUIDs = [deviceUIDs arrayByAddingObject:uid]; } } // Fill in any remaining places in the list with stored devices. Limit the list to three devices // just to match CoreAudio's behaviour. while ((storedPreferredDeviceIdx < storedUIDs.count) && (deviceUIDs.count < 3)) { deviceUIDs = [self addNextStoredPreferredDevice:&storedPreferredDeviceIdx preferredDeviceUIDs:deviceUIDs storedPreferredDeviceUIDs:storedUIDs]; } return deviceUIDs; } - (NSArray*) addNextStoredPreferredDevice:(int*)storedPreferredDeviceIdx preferredDeviceUIDs:(NSArray*)deviceUIDs storedPreferredDeviceUIDs:(NSArray*)storedUIDs { NSString* __nullable storedPreferredDevice; // Try to find a stored UID that isn't already in the list. do { storedPreferredDevice = (*storedPreferredDeviceIdx < storedUIDs.count) ? storedUIDs[*storedPreferredDeviceIdx] : nil; (*storedPreferredDeviceIdx)++; } while (storedPreferredDevice && [deviceUIDs containsObject:BGMNN(storedPreferredDevice)]); // If we found a stored UID, add it to the list. if (storedPreferredDevice) { DebugMsg("BGMPreferredOutputDevices::addNextStoredPreferredDevice: " "Adding stored preferred device: %s", storedPreferredDevice.UTF8String); deviceUIDs = [deviceUIDs arrayByAddingObject:BGMNN(storedPreferredDevice)]; } return deviceUIDs; } - (void) listenForDevicesAddedOrRemoved { // Create the block that will run when a device is added or removed. BGMPreferredOutputDevices* __weak weakSelf = self; _deviceListListener = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) { #pragma unused (inNumberAddresses, inAddresses) BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { [weakSelf connectedDeviceListChanged]; }); }; // Register the listener block with CoreAudio. CAHALAudioSystemObject().AddPropertyListenerBlock( CAPropertyAddress(kAudioHardwarePropertyDevices), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), _deviceListListener); } - (void) connectedDeviceListChanged { @try { [_stateLock lock]; // Decide which device should be the output device now. If a device has been connected and // it's preferred over the current output device, we'll change to that device. If the // current output device has been removed, we'll change to the next most-preferred device. AudioObjectID preferredDevice = [self findPreferredDevice]; if (preferredDevice == kAudioObjectUnknown) { LogWarning("BGMPreferredOutputDevices::connectedDeviceListChanged: " "No preferred device found."); } else if (_devices.outputDevice.GetObjectID() == preferredDevice) { DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: " "The preferred device is already set as the output device."); } else { // Change to the preferred device. DebugMsg("BGMPreferredOutputDevices::connectedDeviceListChanged: " "Changing output device to %d.", preferredDevice); NSError* __nullable error = [_devices setOutputDeviceWithID:preferredDevice revertOnFailure:YES]; if (error) { // There's not much we can do if this happens. LogError("BGMPreferredOutputDevices::connectedDeviceListChanged: " "Failed to change to preferred device. Error: %s", error.debugDescription.UTF8String); } } } @finally { [_stateLock unlock]; } } - (AudioObjectID) findPreferredDevice { AudioObjectID preferredDevice = kAudioObjectUnknown; CAHALAudioSystemObject audioSystem; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { BGMAudioDevice defaultDevice = audioSystem.GetDefaultAudioDevice(false, false); if (!defaultDevice.IsBGMDeviceInstance()) { // BGMDevice isn't the systemwide default device, so we know the default device is the // most-preferred device that's currently connected. preferredDevice = defaultDevice; } }); if (preferredDevice == kAudioObjectUnknown) { // BGMDevice is the systemwide default device, so this method is probably being called after // launch, since we set BGMDevice as the default device then. It could also be that the user // set it manually or that BGMApp failed to change it back the last time it closed. Either // way, we'll try to find a device to use in the Plist or stored list instead. DebugMsg("BGMPreferredOutputDevices::findPreferredDevice: Checking Plist and stored list."); BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { preferredDevice = [self findPreferredDeviceInDerivedList]; }); } BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { if (preferredDevice == kAudioObjectUnknown && [self isCurrentOutputDeviceConnected]) { // The output device (for BGMApp) has been set and is still connected. We haven't found // a better device to use, so prefer leaving the output device as it is. DebugMsg("BGMPreferredOutputDevices::findPreferredDevice: " "Choosing the current output device as the preferred device."); preferredDevice = _devices.outputDevice.GetObjectID(); } }); if (preferredDevice == kAudioObjectUnknown) { // The current output device has been disconnected or hasn't been set yet and there are no // preferred devices connected, so pick one arbitrarily. DebugMsg("BGMPreferredOutputDevices::findPreferredDevice: Choosing an arbitrary device."); BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { preferredDevice = [self findPreferredDeviceByLatency]; }); } return preferredDevice; } // Looks for a suitable device in _preferredDeviceUIDs, the list of preferred devices derived from // CoreAudio's Plist and BGMApp's stored list. - (AudioObjectID) findPreferredDeviceInDerivedList { CAHALAudioSystemObject audioSystem; UInt32 numDevices = audioSystem.GetNumberAudioDevices(); // Get the list of currently connected audio devices. CAAutoArrayDelete devices(numDevices); audioSystem.GetAudioDevices(numDevices, devices); // Look through the preferred devices list to see if one's connected. Return the first one we // find because they're stored in order of preference. for (int position = 0; position < _preferredDeviceUIDs.count; position++) { // Compare the current preferred device to each connected device by UID. for (UInt32 i = 0; i < numDevices; i++) { NSString* __nullable connectedDeviceUID = nil; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { // Skip devices we can't use, e.g. BGMDevice. if (BGMAudioDevice(devices[i]).CanBeOutputDeviceInBGMApp()) { // Get the connected device's UID. connectedDeviceUID = (__bridge NSString* __nullable)CAHALAudioDevice(devices[i]).CopyDeviceUID(); } }); // If the UIDs match, the current preferred device is connected. // // If you plug a USB device in to different USB port, macOS might assign it a different // UID and make it fail to match its old UID here. CoreAudio/macOS doesn't seem to // handle that case either, though, so it's probably not worth worrying about. if ([connectedDeviceUID isEqualToString:_preferredDeviceUIDs[position]]) { // We're iterating through the preferred devices from most to least-preferred, so // we've found the device to use. DebugMsg("BGMPreferredOutputDevices::findPreferredDeviceInDerivedList: " "Found preferred device '%s' at position %d", _preferredDeviceUIDs[position].UTF8String, position); return devices[i]; } } DebugMsg("BGMPreferredOutputDevices::findPreferredDeviceInDerivedList: " "Preferred device not connected: %s", _preferredDeviceUIDs[position].UTF8String); } return kAudioObjectUnknown; } - (bool) isCurrentOutputDeviceConnected { if (_devices.outputDevice.GetObjectID() == kAudioObjectUnknown) { DebugMsg("BGMPreferredOutputDevices::isCurrentOutputDeviceConnected: " "The output device hasn't been set yet."); return false; } CAHALAudioSystemObject audioSystem; UInt32 numDevices = audioSystem.GetNumberAudioDevices(); // Get the list of currently connected audio devices. CAAutoArrayDelete devices(numDevices); audioSystem.GetAudioDevices(numDevices, devices); // Look for the current output device in the list of connected devices. for (UInt32 i = 0; i < numDevices; i++) { // TODO: Are AudioObjectIDs reused? If they are, could that cause a collision here? if (_devices.outputDevice.GetObjectID() == devices[i]) { DebugMsg("BGMPreferredOutputDevices::isCurrentOutputDeviceConnected: " "The output device is connected."); return true; } } DebugMsg("BGMPreferredOutputDevices::isCurrentOutputDeviceConnected: " "The output device is not connected."); return false; } // Returns the audio output device with the lowest latency. Used when we have no better way to // choose the output device for BGMApp to use. - (AudioObjectID) findPreferredDeviceByLatency { CAHALAudioSystemObject audioSystem; UInt32 numDevices = audioSystem.GetNumberAudioDevices(); AudioObjectID minLatencyDevice = kAudioObjectUnknown; UInt32 minLatency = UINT32_MAX; CAAutoArrayDelete devices(numDevices); audioSystem.GetAudioDevices(numDevices, devices); for (UInt32 i = 0; i < numDevices; i++) { BGMAudioDevice device(devices[i]); if (!device.IsBGMDeviceInstance()) { BOOL hasOutputChannels = NO; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "GetTotalNumberChannels", [&] { hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0; }); if (hasOutputChannels) { BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, "GetLatency", [&] { UInt32 latency = device.GetLatency(false); if (latency < minLatency) { minLatencyDevice = devices[i]; minLatency = latency; } }); } } } return minLatencyDevice; } - (void) userChangedOutputDeviceTo:(AudioObjectID)device { @try { [_stateLock lock]; BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { // Add the new output device to the list. NSString* __nullable outputDeviceUID = (__bridge_transfer NSString* __nullable)CAHALAudioDevice(device).CopyDeviceUID(); if (outputDeviceUID) { // Limit the list to three devices because that's what macOS does. if (_preferredDeviceUIDs.count >= 2) { _preferredDeviceUIDs = @[BGMNN(outputDeviceUID), _preferredDeviceUIDs[0], _preferredDeviceUIDs[1]]; } else if (_preferredDeviceUIDs.count >= 1) { _preferredDeviceUIDs = @[BGMNN(outputDeviceUID), _preferredDeviceUIDs[0]]; } else { _preferredDeviceUIDs = @[BGMNN(outputDeviceUID)]; } DebugMsg("BGMPreferredOutputDevices::userChangedOutputDeviceTo: " "Preferred devices: %s", _preferredDeviceUIDs.debugDescription.UTF8String); // Save the list. _userDefaults.preferredDeviceUIDs = _preferredDeviceUIDs; } else { LogWarning("BGMPreferredOutputDevices::userChangedOutputDeviceTo: " "Output device has no UID"); } }); } @finally { [_stateLock unlock]; } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMStatusBarItem.h ================================================ // 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 . // // BGMStatusBarItem.h // BGMApp // // Copyright © 2019, 2020 Kyle Neideck // // The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main // menu for the app. These are called "menu bar extras" in the Human Interface Guidelines. // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMDebugLoggingMenuItem.h" // System Includes #import // Forward Declarations @class BGMUserDefaults; #pragma clang assume_nonnull begin typedef NS_ENUM(NSInteger, BGMStatusBarIcon) { BGMFermataStatusBarIcon = 0, BGMVolumeStatusBarIcon }; static BGMStatusBarIcon const kBGMStatusBarIconMinValue = BGMFermataStatusBarIcon; static BGMStatusBarIcon const kBGMStatusBarIconMaxValue = BGMVolumeStatusBarIcon; static BGMStatusBarIcon const kBGMStatusBarIconDefaultValue = BGMFermataStatusBarIcon; @interface BGMStatusBarItem : NSObject - (instancetype) initWithMenu:(NSMenu*)bgmMenu audioDevices:(BGMAudioDeviceManager*)devices userDefaults:(BGMUserDefaults*)defaults; // Set this to BGMFermataStatusBarIcon to change the icon to the Background Music logo. // // Set this to BGMFermataStatusBarIcon to change the icon to a volume icon. This icon has the // advantage of indicating the volume level, but we can't make it the default because it looks the // same as the icon for the macOS volume status bar item. @property BGMStatusBarIcon icon; // If the user holds down the option key when they click the status bar icon, this menu item will be // shown in the main menu. - (void) setDebugLoggingMenuItem:(BGMDebugLoggingMenuItem*)menuItem; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMStatusBarItem.mm ================================================ // 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 . // // BGMStatusBarItem.m // BGMApp // // Copyright © 2019, 2020 Kyle Neideck // // Self Include #import "BGMStatusBarItem.h" // Local Includes #import "BGM_Utils.h" #import "BGMUserDefaults.h" #import "BGMVolumeChangeListener.h" #pragma clang assume_nonnull begin static CGFloat const kStatusBarIconPadding = 0.25; static CGFloat const kVolumeIconAdditionalVerticalPadding = 0.075; @implementation BGMStatusBarItem { BGMAudioDeviceManager* audioDevices; // User settings and data. BGMUserDefaults* userDefaults; NSImage* fermataIcon; NSImage* volumeIcon0SoundWaves; NSImage* volumeIcon1SoundWave; NSImage* volumeIcon2SoundWaves; NSImage* volumeIcon3SoundWaves; NSStatusItem* statusBarItem; BGMDebugLoggingMenuItem* debugLoggingMenuItem; BGMVolumeChangeListener* volumeChangeListener; id __nullable clickEventHandler; BGMStatusBarIcon _icon; } #pragma mark Initialisation - (instancetype) initWithMenu:(NSMenu*)bgmMenu audioDevices:(BGMAudioDeviceManager*)devices userDefaults:(BGMUserDefaults*)defaults { if ((self = [super init])) { statusBarItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; audioDevices = devices; userDefaults = defaults; // Initialise the icons. [self initIcons]; // Set the initial icon. self.icon = userDefaults.statusBarIcon; // Set the menu item to open the main menu. statusBarItem.menu = bgmMenu; // Monitor click events so we can show extra options in the menu if the user was holding the // option key. clickEventHandler = [self addClickMonitor]; // Set the accessibility label to "Background Music". (We intentionally don't set a title or // a tooltip.) if ([BGMStatusBarItem buttonAvailable]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" statusBarItem.button.accessibilityLabel = [NSRunningApplication currentApplication].localizedName; #pragma clang diagnostic pop } // Update the icon when BGMDevice's volume changes. BGMStatusBarItem* __weak weakSelf = self; volumeChangeListener = new BGMVolumeChangeListener(audioDevices.bgmDevice, [=] { [weakSelf bgmDeviceVolumeDidChange]; }); } return self; } - (id __nullable) addClickMonitor { NSEvent* __nullable (^handlerBlock)(NSEvent*) = ^NSEvent* __nullable (NSEvent* event) { [self statusBarItemWasClicked:event]; return event; }; // TODO: I doubt this works well with VoiceOver. return [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskLeftMouseDown handler:handlerBlock]; } - (void) dealloc { delete volumeChangeListener; if (clickEventHandler) { [NSEvent removeMonitor:(id)clickEventHandler]; clickEventHandler = nil; } } - (void) initIcons { // Load the icons. fermataIcon = [NSImage imageNamed:@"FermataIcon"]; if (@available(macOS 11.0, *)) { volumeIcon0SoundWaves = [NSImage imageWithSystemSymbolName:@"speaker.fill" accessibilityDescription:nil]; volumeIcon1SoundWave = [NSImage imageWithSystemSymbolName:@"speaker.wave.1.fill" accessibilityDescription:nil]; volumeIcon2SoundWaves = [NSImage imageWithSystemSymbolName:@"speaker.wave.2.fill" accessibilityDescription:nil]; volumeIcon3SoundWaves = [NSImage imageWithSystemSymbolName:@"speaker.wave.3.fill" accessibilityDescription:nil]; } else { volumeIcon0SoundWaves = [NSImage imageNamed:@"Volume0"]; volumeIcon1SoundWave = [NSImage imageNamed:@"Volume1"]; volumeIcon2SoundWaves = [NSImage imageNamed:@"Volume2"]; volumeIcon3SoundWaves = [NSImage imageNamed:@"Volume3"]; } // Set the icons' sizes. NSRect statusBarItemFrame; if ([BGMStatusBarItem buttonAvailable]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" statusBarItemFrame = statusBarItem.button.frame; #pragma clang diagnostic pop } else { // OS X 10.9 fallback. I haven't tested this (or anything else on 10.9). statusBarItemFrame = statusBarItem.view.frame; } CGFloat heightMinusPadding = statusBarItemFrame.size.height * (1 - kStatusBarIconPadding); // The fermata icon has equal width and height. [fermataIcon setSize:NSMakeSize(heightMinusPadding, heightMinusPadding)]; // The volume icons are all the same width and height. CGFloat volumeIconWidthToHeightRatio = volumeIcon0SoundWaves.size.width / volumeIcon0SoundWaves.size.height; CGFloat volumeIconWidth = heightMinusPadding * volumeIconWidthToHeightRatio; CGFloat volumeIconHeight = heightMinusPadding * (1 - kVolumeIconAdditionalVerticalPadding); [volumeIcon0SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; [volumeIcon1SoundWave setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; [volumeIcon2SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; [volumeIcon3SoundWaves setSize:NSMakeSize(volumeIconWidth, volumeIconHeight)]; // Make the icons "template images" so they get drawn colour-inverted when they're highlighted // or the system is in dark mode. [fermataIcon setTemplate:YES]; [volumeIcon0SoundWaves setTemplate:YES]; [volumeIcon1SoundWave setTemplate:YES]; [volumeIcon2SoundWaves setTemplate:YES]; [volumeIcon3SoundWaves setTemplate:YES]; } #pragma mark Accessors + (BOOL) buttonAvailable { // NSStatusItem doesn't have the "button" property on OS X 10.9. return (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_10); } - (void) setImage:(NSImage*)image { if ([BGMStatusBarItem buttonAvailable]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" statusBarItem.button.image = image; #pragma clang diagnostic pop } else { statusBarItem.image = image; } } - (BGMStatusBarIcon) icon { return _icon; } - (void) setIcon:(BGMStatusBarIcon)icon { _icon = icon; // Save the setting. userDefaults.statusBarIcon = self.icon; // Change the icon (i.e. the image). Dispatch this to the main thread because it changes the UI. dispatch_async(dispatch_get_main_queue(), ^{ if (_icon == BGMFermataStatusBarIcon) { [self setImage:fermataIcon]; // If the icon was greyed out, change it back. if ([BGMStatusBarItem buttonAvailable]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" statusBarItem.button.appearsDisabled = NO; #pragma clang diagnostic pop } } else { BGMAssert((_icon == BGMVolumeStatusBarIcon), "Unknown icon in enum"); [self updateVolumeStatusBarIcon]; } }); } #pragma mark Volume Icon - (void) bgmDeviceVolumeDidChange { if (self.icon == BGMVolumeStatusBarIcon) { [self updateVolumeStatusBarIcon]; } } // Should only be called on the main thread because it calls UI functions. - (void) updateVolumeStatusBarIcon { BGMAssert([[NSThread currentThread] isMainThread], "updateVolumeStatusBarIcon called on non-main thread."); BGMAssert((self.icon == BGMVolumeStatusBarIcon), "Volume status bar icon not enabled"); BGMAudioDevice bgmDevice = [audioDevices bgmDevice]; // BGMDevice should never return an error for these calls, so we just swallow any exceptions and // give up. BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput; AudioObjectPropertyScope element = kAudioObjectPropertyElementMaster; BOOL hasVolume = bgmDevice.HasVolumeControl(scope, element); // Show the button as greyed out if BGMDevice doesn't have a volume control (which means the // output device doesn't have one). if ([BGMStatusBarItem buttonAvailable]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wpartial-availability" statusBarItem.button.appearsDisabled = !hasVolume; #pragma clang diagnostic pop } if (hasVolume) { if (bgmDevice.HasMuteControl(scope, element) && bgmDevice.GetMuteControlValue(scope, element)) { // The device is muted, so use the zero waves icon. [self setImage:volumeIcon0SoundWaves]; } else { // Set the icon to reflect the device's volume. double volume = bgmDevice.GetVolumeControlScalarValue(scope, element); // These values match the macOS volume status bar item, except for the first one. I // don't know why, but at a very low volume macOS will show the zero waves icon even // though the sound is still audible. if (volume == 0.05) { [self setImage:volumeIcon0SoundWaves]; } else if (volume < 0.33) { [self setImage:volumeIcon1SoundWave]; } else if (volume < 0.66) { [self setImage:volumeIcon2SoundWaves]; } else { [self setImage:volumeIcon3SoundWaves]; } } } else { // Always use the full-volume icon when the device has no volume control. [self setImage:volumeIcon3SoundWaves]; } }); DebugMsg("BGMStatusBarItem::updateVolumeStatusBarIcon: Set icon to %s", statusBarItem.image.name.UTF8String); } #pragma mark Debug Logging Menu Item - (void) statusBarItemWasClicked:(NSEvent* __nonnull)event { if ((event.modifierFlags & NSEventModifierFlagOption) != 0) { DebugMsg("BGMStatusBarItem::statusBarItemWasClicked: Option key held"); [debugLoggingMenuItem setMenuShowingExtraOptions:YES]; } else { [debugLoggingMenuItem setMenuShowingExtraOptions:NO]; } } - (void) setDebugLoggingMenuItem:(BGMDebugLoggingMenuItem*)menuItem { debugLoggingMenuItem = menuItem; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMSystemSoundsVolume.h ================================================ // 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 . // // 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 #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 ================================================ FILE: BGMApp/BGMApp/BGMSystemSoundsVolume.mm ================================================ // 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 . // // 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 ================================================ FILE: BGMApp/BGMApp/BGMTermination.h ================================================ // 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 . // // 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 #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 */ ================================================ FILE: BGMApp/BGMApp/BGMTermination.mm ================================================ // 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 . // // 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 // System Includes #import #import #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 // // and // // (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(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 ================================================ FILE: BGMApp/BGMApp/BGMUserDefaults.h ================================================ // 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 . // // BGMUserDefaults.h // BGMApp // // Copyright © 2016-2019 Kyle Neideck // // A simple wrapper around our use of NSUserDefaults. Used to store the preferences/state that only // apply to BGMApp. The others are stored by BGMDriver. // // Private data will be stored in the user's keychain instead of user defaults. // // Local Includes #import "BGMStatusBarItem.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMUserDefaults : NSObject // If inDefaults is nil, settings are not loaded from or saved to disk, which is useful for testing. - (instancetype) initWithDefaults:(NSUserDefaults* __nullable)inDefaults; // The musicPlayerID (see BGMMusicPlayer.h), as a string, of the music player selected by the user. // Must be either null or a string that can be parsed by NSUUID. @property NSString* __nullable selectedMusicPlayerID; @property BOOL autoPauseMusicEnabled; // The UIDs of the output devices most recently selected by the user. The most-recently selected // device is at index 0. See BGMPreferredOutputDevices. @property NSArray* preferredDeviceUIDs; // The (type of) icon to show in the button in the status bar. (The button the user clicks to open // BGMApp's main menu.) @property BGMStatusBarIcon statusBarIcon; // The auth code we're required to send when connecting to GPMDP. Stored in the keychain. Reading // this property is thread-safe, but writing it isn't. // // Returns nil if no code is found or if reading fails. If writing fails, an error is logged, but no // exception is thrown. @property NSString* __nullable googlePlayMusicDesktopPlayerPermanentAuthCode; // Auto-pause delay settings in milliseconds. These control how long to wait before pausing/unpausing // music when other audio starts/stops playing. @property NSUInteger pauseDelayMS; @property NSUInteger maxUnpauseDelayMS; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMUserDefaults.m ================================================ // 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 . // // BGMUserDefaults.m // BGMApp // // Copyright © 2016-2019 Kyle Neideck // // Self Include #import "BGMUserDefaults.h" // Local Includes #import "BGM_Utils.h" #pragma clang assume_nonnull begin // Keys static NSString* const kDefaultKeyAutoPauseMusicEnabled = @"AutoPauseMusicEnabled"; static NSString* const kDefaultKeySelectedMusicPlayerID = @"SelectedMusicPlayerID"; static NSString* const kDefaultKeyPreferredDeviceUIDs = @"PreferredDeviceUIDs"; static NSString* const kDefaultKeyStatusBarIcon = @"StatusBarIcon"; static NSString* const kDefaultKeyPauseDelayMS = @"PauseDelayMS"; static NSString* const kDefaultKeyMaxUnpauseDelayMS = @"MaxUnpauseDelayMS"; // Labels for Keychain Data static NSString* const kKeychainLabelGPMDPAuthCode = @"app.backgroundmusic: Google Play Music Desktop Player permanent auth code"; @implementation BGMUserDefaults { // The defaults object wrapped by this object. NSUserDefaults* defaults; // When we're not persisting defaults, settings are stored in this dictionary instead. This // var should only be accessed if 'defaults' is nil. NSMutableDictionary* transientDefaults; } - (instancetype) initWithDefaults:(NSUserDefaults* __nullable)inDefaults { if ((self = [super init])) { defaults = inDefaults; // Register the settings defaults. // // iTunes is the default music player, but we don't set kDefaultKeySelectedMusicPlayerID // here so we know when it's never been set. (If it hasn't, we try using BGMDevice's // kAudioDeviceCustomPropertyMusicPlayerBundleID property to tell which music player should // be selected. See BGMMusicPlayers.) NSDictionary* defaultsDict = @{ kDefaultKeyAutoPauseMusicEnabled: @YES, kDefaultKeyPauseDelayMS: @1500, kDefaultKeyMaxUnpauseDelayMS: @3500 }; if (defaults) { [defaults registerDefaults:defaultsDict]; } else { transientDefaults = [defaultsDict mutableCopy]; } } return self; } #pragma mark Selected Music Player - (NSString* __nullable) selectedMusicPlayerID { return [self get:kDefaultKeySelectedMusicPlayerID]; } - (void) setSelectedMusicPlayerID:(NSString* __nullable)selectedMusicPlayerID { [self set:kDefaultKeySelectedMusicPlayerID to:selectedMusicPlayerID]; } #pragma mark Auto-pause - (BOOL) autoPauseMusicEnabled { return [self getBool:kDefaultKeyAutoPauseMusicEnabled]; } - (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled { [self setBool:kDefaultKeyAutoPauseMusicEnabled to:autoPauseMusicEnabled]; } #pragma mark Auto-pause Delays - (NSUInteger) pauseDelayMS { NSInteger delay = [self getInt:kDefaultKeyPauseDelayMS or:1500]; // Clamp to reasonable range: 0ms to 10000ms delay = MAX(0, MIN(10000, delay)); return (NSUInteger)delay; } - (void) setPauseDelayMS:(NSUInteger)pauseDelayMS { // Clamp to reasonable range: 0ms to 10000ms NSUInteger clampedDelay = MAX(0, MIN(10000, pauseDelayMS)); [self setInt:kDefaultKeyPauseDelayMS to:(NSInteger)clampedDelay]; } - (NSUInteger) maxUnpauseDelayMS { NSInteger delay = [self getInt:kDefaultKeyMaxUnpauseDelayMS or:3500]; // Clamp to reasonable range: 0ms to 10000ms delay = MAX(0, MIN(10000, delay)); return (NSUInteger)delay; } - (void) setMaxUnpauseDelayMS:(NSUInteger)maxUnpauseDelayMS { // Clamp to reasonable range: 0ms to 10000ms NSUInteger clampedDelay = MAX(0, MIN(10000, maxUnpauseDelayMS)); [self setInt:kDefaultKeyMaxUnpauseDelayMS to:(NSInteger)clampedDelay]; } - (NSArray*) preferredDeviceUIDs { NSArray* __nullable uids = [self get:kDefaultKeyPreferredDeviceUIDs]; return uids ? BGMNN(uids) : @[]; } - (void) setPreferredDeviceUIDs:(NSArray*)devices { [self set:kDefaultKeyPreferredDeviceUIDs to:devices]; } - (BGMStatusBarIcon) statusBarIcon { NSInteger icon = [self getInt:kDefaultKeyStatusBarIcon or:kBGMStatusBarIconDefaultValue]; // Just in case we get an invalid value somehow. if ((icon < kBGMStatusBarIconMinValue) || (icon > kBGMStatusBarIconMaxValue)) { NSLog(@"BGMUserDefaults::statusBarIcon: Unknown BGMStatusBarIcon: %ld", (long)icon); icon = kBGMStatusBarIconDefaultValue; } return (BGMStatusBarIcon)icon; } - (void) setStatusBarIcon:(BGMStatusBarIcon)icon { [self setInt:kDefaultKeyStatusBarIcon to:icon]; } #pragma mark Google Play Music Desktop Player - (NSString* __nullable) googlePlayMusicDesktopPlayerPermanentAuthCode { // Try to read the permanent auth code from the user's keychain. NSDictionary* query = @{ (__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword, (__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode, (__bridge NSString*)kSecMatchLimit: (__bridge NSString*)kSecMatchLimitOne, (__bridge NSString*)kSecReturnData: @YES }; CFTypeRef result = nil; OSStatus err = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result); NSString* __nullable authCode = nil; // Check the return status, null check and check the type. if ((err == errSecSuccess) && result && (CFGetTypeID(result) == CFDataGetTypeID())) { // Convert it to a string. CFStringRef __nullable code = CFStringCreateFromExternalRepresentation(kCFAllocatorDefault, result, kCFStringEncodingUTF8); authCode = (__bridge_transfer NSString* __nullable)code; } else if (err != errSecItemNotFound) { NSString* __nullable errMsg = (__bridge_transfer NSString* __nullable)SecCopyErrorMessageString(err, nil); NSLog(@"Failed to read GPMDP auth code from keychain: %d, %@", err, errMsg); } // Release the data we read. if (result) { CFRelease(result); } return authCode; } - (void) setGooglePlayMusicDesktopPlayerPermanentAuthCode:(NSString* __nullable)authCode { if (authCode) { // Convert it to an NSData so we can store it in the user's keychain. NSData* authCodeData = [authCode dataUsingEncoding:NSUTF8StringEncoding]; // Delete the old code if necessary. (There's an update function, but this takes less code.) if (self.googlePlayMusicDesktopPlayerPermanentAuthCode) { [self deleteGPMDPPermanentAuthCode]; } // Store the code. [self addGPMDPPermanentAuthCode:authCodeData]; } else { [self deleteGPMDPPermanentAuthCode]; } } - (void) addGPMDPPermanentAuthCode:(NSData*)authCodeData { NSDictionary* attributes = @{ (__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword, (__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode, (__bridge NSString*)kSecValueData: authCodeData }; OSStatus err = SecItemAdd((__bridge CFDictionaryRef)attributes, nil); // Just log an error if it failed. if (err != errSecSuccess) { NSString* errMsg = (__bridge_transfer NSString*)SecCopyErrorMessageString(err, nil); NSLog(@"Failed to store GPMDP auth code in keychain: %d, %@", err, errMsg); } } - (void) deleteGPMDPPermanentAuthCode { NSDictionary* query = @{ (__bridge NSString*)kSecClass: (__bridge NSString*)kSecClassGenericPassword, (__bridge NSString*)kSecAttrLabel: kKeychainLabelGPMDPAuthCode }; OSStatus err = SecItemDelete((__bridge CFDictionaryRef)query); // Just log an error if it failed. if (err != errSecSuccess) { NSString* errMsg = (__bridge_transfer NSString*)SecCopyErrorMessageString(err, nil); NSLog(@"Failed to delete GPMDP auth code from keychain: %d, %@", err, errMsg); } } #pragma mark General Accessors - (id __nullable) get:(NSString*)key { return defaults ? [defaults objectForKey:key] : transientDefaults[key]; } - (void) set:(NSString*)key to:(NSObject* __nullable)value { if (defaults) { [defaults setObject:value forKey:key]; } else { transientDefaults[key] = value; } } // TODO: This method should have a default value param. - (BOOL) getBool:(NSString*)key { return defaults ? [defaults boolForKey:key] : [transientDefaults[key] boolValue]; } - (void) setBool:(NSString*)key to:(BOOL)value { if (defaults) { [defaults setBool:value forKey:key]; } else { transientDefaults[key] = @(value); } } - (NSInteger) getInt:(NSString*)key or:(NSInteger)valueIfNil { if (defaults) { if ([defaults objectForKey:key]) { return [defaults integerForKey:key]; } else { return valueIfNil; } } else { if (transientDefaults[key]) { return [transientDefaults[key] intValue]; } else { return valueIfNil; } } } - (void) setInt:(NSString*)key to:(NSInteger)value { if (defaults) { [defaults setInteger:value forKey:key]; } else { transientDefaults[key] = @(value); } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMVolumeChangeListener.cpp ================================================ // 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 . // // BGMVolumeChangeListener.cpp // BGMApp // // Copyright © 2019 Kyle Neideck // // Self Include #include "BGMVolumeChangeListener.h" // Local Includes #import "BGM_Utils.h" #import "BGMAudioDevice.h" // PublicUtility Includes #import "CAException.h" #import "CAPropertyAddress.h" #pragma clang assume_nonnull begin #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wexit-time-destructors" const static std::vector kVolumeChangeProperties = { // Output volume changes CAPropertyAddress(kAudioDevicePropertyVolumeScalar, kAudioObjectPropertyScopeOutput), // Mute/unmute CAPropertyAddress(kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput), // Received when controls are added to or removed from the device. CAPropertyAddress(kAudioObjectPropertyControlList), // Received when the device has changed and "clients should re-evaluate everything they need // to know about the device, particularly the layout and values of the controls". CAPropertyAddress(kAudioDevicePropertyDeviceHasChanged) }; #pragma clang diagnostic pop BGMVolumeChangeListener::BGMVolumeChangeListener(BGMAudioDevice device, std::function handler) : mDevice(device) { // Register a listener that will update the slider when the user changes the volume or // mutes/unmutes their audio. mListenerBlock = Block_copy(^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) { // The docs for AudioObjectPropertyListenerBlock say inAddresses will always contain // at least one property the block is listening to, so there's no need to check it. (void)inNumberAddresses; (void)inAddresses; // Call the callback. handler(); }); // Register for a number of properties that might indicate that clients need to update. For // example, the mute property changing means UI elements that display the volume will need to be // updated, even though it's not strictly a change in volume. for(CAPropertyAddress property : kVolumeChangeProperties) { // Instead of swallowing exceptions here, we could try again later, but I doubt it would be // worth the effort. And the documentation doesn't actually explain what could cause this // call to fail. BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { mDevice.AddPropertyListenerBlock(property, dispatch_get_main_queue(), mListenerBlock); }); } } BGMVolumeChangeListener::~BGMVolumeChangeListener() { // Deregister and release the listener block. for(CAPropertyAddress property : kVolumeChangeProperties) { BGM_Utils::LogAndSwallowExceptions(BGMDbgArgs, [&] { mDevice.RemovePropertyListenerBlock(property, dispatch_get_main_queue(), mListenerBlock); }); } Block_release(mListenerBlock); } #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMVolumeChangeListener.h ================================================ // 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 . // // BGMVolumeChangeListener.h // BGMApp // // Copyright © 2019 Kyle Neideck // // Local Includes #include "BGMBackgroundMusicDevice.h" // PublicUtility Includes #import "CAPropertyAddress.h" // STL Includes #include // System Includes #include #pragma clang assume_nonnull begin class BGMVolumeChangeListener { public: /*! * @param device Listens for notifications about this device. * @param handler The function to call when the device's volume (or mute) changes. Called on the * main queue. */ BGMVolumeChangeListener(BGMAudioDevice device, std::function handler); virtual ~BGMVolumeChangeListener(); BGMVolumeChangeListener(const BGMVolumeChangeListener&) = delete; BGMVolumeChangeListener& operator=(const BGMVolumeChangeListener&) = delete; private: AudioObjectPropertyListenerBlock mListenerBlock; BGMAudioDevice mDevice; }; #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMXPCListener.h ================================================ // 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 . // // BGMXPCListener.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Connects to BGMXPCHelper via XPC. When BGMDriver wants BGMApp to do something it can call one of BGMHelper's // XPC methods, which passes the request along to this class. // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMXPCProtocols.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMXPCListener : NSObject - (id) initWithAudioDevices:(BGMAudioDeviceManager*)devices helperConnectionErrorHandler:(void (^)(NSError* error))errorHandler; - (void) initHelperConnection; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/BGMXPCListener.mm ================================================ // 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 . // // BGMXPCListener.mm // BGMApp // // Copyright © 2016, 2017 Kyle Neideck // // Self Include #import "BGMXPCListener.h" // Local Includes #import "BGMPlayThrough.h" // For kDeviceNotStarting. #pragma clang assume_nonnull begin @implementation BGMXPCListener { NSXPCListener* listener; // The connection to BGMXPCHelper. We keep the connection alive so if BGMXPCHelper is killed or crashes our interruptionHandler // runs and we can re-register our listener endpoint. Otherwise BGMXPCHelper would have no way to initiate connections to BGMApp. // (That's because BGMXPCHelper is in the global bootstrap context, so it can't look BGMApp up, and XPC services are supposed to // be as stateless as possible, so when it restarts it doesn't restore its copy of BGMApp's listener endpoint.) NSXPCConnection* __nullable helperConnection; BGMAudioDeviceManager* audioDevices; // Used to regularly try reconnecting to BGMXPCHelper if the connection has failed. NSTimer* __nullable retryTimer; } - (id) initWithAudioDevices:(BGMAudioDeviceManager*)devices helperConnectionErrorHandler:(void (^)(NSError* error))errorHandler { if ((self = [super init])) { audioDevices = devices; // Create BGMApp's local listener. listener = [NSXPCListener anonymousListener]; // Set this class as the listener's delegate so our listener:shouldAcceptNewConnection method handles incoming connections. listener.delegate = self; [listener resume]; // 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; } - (void) initHelperConnection { // Note that this is called when the helper connection's interruption handler is retrying the connection after a timeout. [self initHelperConnectionWithErrorHandler:^(NSError* error) { #if !DEBUG #pragma unused (error) #endif DebugMsg("BGMXPCListener::initHelperConnection: Connection error: %s", [[error description] UTF8String]); }]; } - (void) initHelperConnectionWithErrorHandler:(void (^)(NSError* error))errorHandler { // Connect to the helper service so we can call its remote methods (and vice versa). // Clean up if there's an existing connection. if (helperConnection) { [helperConnection invalidate]; helperConnection = nil; } // Uses the NSXPCConnectionPrivileged option because BGMXPCHelper has to run in the privileged/global bootstrap context for // BGMDriver to be able to look it up. BGMDriver runs in the coreaudiod process, which runs in the global context, and services // in the global context are only able to look up other services in that context. helperConnection = [[NSXPCConnection alloc] initWithMachServiceName:kBGMXPCHelperMachServiceName options:NSXPCConnectionPrivileged]; helperConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BGMXPCHelperXPCProtocol)]; // Export this object so BGMXPCHelper can send messages through helperConnection if it wants to. helperConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BGMAppXPCProtocol)]; helperConnection.exportedObject = self; // If we lose the connection to BGMXPCHelper, we try to reconnect every 10 seconds. void (^retryAfterTimeout)(NSError*) = ^(NSError* error) { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"BGMXPCListener::initHelperConnectionWithErrorHandler: Lost connection to BGMXPCHelper. Will try to reconnect " "every 10 seconds. Details: %@", error); if (!retryTimer) { retryTimer = [NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:@selector(initHelperConnection) userInfo:nil repeats:YES]; } }); }; // If the XPC helper crashes or is killed, this handler gets invoked and attempts to reestablish the connection by sending // a message. NSXPCConnection* __weak helperConnectionWeakRef = helperConnection; NSXPCListener* __weak listenerWeakRef = listener; helperConnection.interruptionHandler = ^{ DebugMsg("BGMXPCListener::initHelperConnectionWithErrorHandler: Connection to BGMXPCHelper interrupted. Trying to reconnect."); [[helperConnectionWeakRef remoteObjectProxyWithErrorHandler:retryAfterTimeout] registerAsBGMAppWithListenerEndpoint:[listenerWeakRef endpoint] reply:^{}]; }; // Use the initialization error handler while we send the initial message. This is so we can warn the user, since failing at // start up probably means we won't be able to recover. helperConnection.invalidationHandler = ^{ errorHandler([NSError errorWithDomain:@kBGMAppBundleID code:kBGMXPC_MessageFailure userInfo:@{ NSLocalizedDescriptionKey: @"Failed to send initial message to BGMXPCHelper." }]); }; [helperConnection resume]; // Send the listener's endpoint to BGMXPCHelper so it can initiate connections to BGMApp. [[helperConnection remoteObjectProxyWithErrorHandler:errorHandler] registerAsBGMAppWithListenerEndpoint:[listener endpoint] reply:^{ // If we were retrying the connection, we can stop now. if (retryTimer) { NSLog(@"BGMXPCListener::initHelperConnectionWithErrorHandler: Reconnected to BGMXPCHelper."); [retryTimer invalidate]; retryTimer = nil; } // Silently retry the connection, after a timeout, if it fails after having worked initially. helperConnection.invalidationHandler = ^{ DebugMsg("BGMXPCListener::initHelperConnectionWithErrorHandler: Connection to BGMXPCHelper invalidated"); helperConnection = nil; retryAfterTimeout(nil); }; }]; // Pass the new connection to the audio device manager. [audioDevices setBGMXPCHelperConnection:helperConnection]; } - (void) dealloc { if (retryTimer) { [retryTimer invalidate]; } [[helperConnection remoteObjectProxy] unregisterAsBGMApp]; } - (BOOL) listener:(NSXPCListener*)listener shouldAcceptNewConnection:(NSXPCConnection*)newConnection { #pragma unused (listener) // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. DebugMsg("BGMXPCListener::listener: Received new connection"); // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BGMAppXPCProtocol)]; // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to // the exported object to handle. The connection retains the exported object. newConnection.exportedObject = self; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the // connection for some reason, call -invalidate on the connection and return NO. return YES; } - (void) startPlayThroughSyncWithReply:(void (^)(NSError*))reply forUISoundsDevice:(BOOL)isUI { NSString* description; OSStatus err; try { err = [audioDevices startPlayThroughSync:isUI]; } catch (CAException e) { // 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::startPlayThroughSyncWithReply: Caught unknown exception. Replying kBGMXPC_InternalError."); err = kBGMXPC_InternalError; #if DEBUG throw; #endif } switch (err) { case kAudioHardwareNoError: description = @"BGMApp started the output device."; err = kBGMXPC_Success; break; case kAudioHardwareNotRunningError: description = @"BGMApp is not ready for audio play-through."; err = kBGMXPC_BGMAppStateError; break; case kAudioHardwareIllegalOperationError: description = @"The output device is not available."; err = kBGMXPC_HardwareError; break; case kBGMErrorCode_ReturningEarly: // We have to send a more specific error in this case because BGMDevice handles this case differently. description = @"BGMApp could not wait for the output device to be ready for IO."; err = kBGMXPC_ReturningEarlyError; break; default: description = @"Unknown error while waiting for the output device."; err = kBGMXPC_InternalError; break; } reply([NSError errorWithDomain:@kBGMAppBundleID code:err userInfo:@{ NSLocalizedDescriptionKey: description }]); } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Base.lproj/MainMenu.xib ================================================ NSAllRomanInputSourcesLocaleIdentifier // The version and copyright strings are replaced with
// the values from Info.plist in the setup code. These
// values are just placeholders/fallbacks.
//
// The icon image was being drawn with about 50% // opacity and I couldn't figure out why. The only 
// solution I found was to layer two copies of the icon
// on top of each other and add a black shadow (with no
// offset) to both.
 YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T S2V5ZWRBcmNoaXZlctEICVRyb290gAGuCwwbHCEaJissMzY5P0JVJG51bGzXDQ4PEBESExQVFhcYGRpW TlNTaXplViRjbGFzc1xOU0ltYWdlRmxhZ3NWTlNSZXBzW05TVGludENvbG9yV05TQ29sb3JeTlNSZXNp emluZ01vZGWAAoANEiDDAACAA4AAgAsQAFZ7MSwgMX3SHQ4eIFpOUy5vYmplY3RzoR+ABIAK0h0OIiWi IySABYAGgAnTDicoKSoaXxAUTlNUSUZGUmVwcmVzZW50YXRpb25fEBlOU0ludGVybmFsTGF5b3V0RGly ZWN0aW9ugAiAB08RCMRNTQAqAAAACgAAABABAAADAAAAAQABAAABAQADAAAAAQABAAABAgADAAAAAgAI AAgBAwADAAAAAQABAAABBgADAAAAAQABAAABCgADAAAAAQABAAABEQAEAAAAAQAAAAgBEgADAAAAAQAB AAABFQADAAAAAQACAAABFgADAAAAAQABAAABFwAEAAAAAQAAAAIBHAADAAAAAQABAAABKAADAAAAAQAC AAABUgADAAAAAQABAAABUwADAAAAAgABAAGHcwAHAAAH9AAAANAAAAAAAAAH9GFwcGwCIAAAbW50ckdS QVlYWVogB9AAAgAOAAwAAAAAYWNzcEFQUEwAAAAAbm9uZQAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA 0y1hcHBsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFZGVzYwAA AMAAAABvZHNjbQAAATAAAAZmY3BydAAAB5gAAAA4d3RwdAAAB9AAAAAUa1RSQwAAB+QAAAAOZGVzYwAA AAAAAAAVR2VuZXJpYyBHcmF5IFByb2ZpbGUAAAAAAAAAAAAAABVHZW5lcmljIEdyYXkgUHJvZmlsZQAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG1sdWMAAAAAAAAAHwAA AAxza1NLAAAAKgAAAYRlblVTAAAAKAAAAa5jYUVTAAAALAAAAdZ2aVZOAAAALAAAAgJwdEJSAAAAKgAA Ai51a1VBAAAALAAAAlhmckZVAAAAKgAAAoRodUhVAAAALgAAAq56aFRXAAAAEAAAAtxuYk5PAAAALAAA Auxrb0tSAAAAGAAAAxhjc0NaAAAAJAAAAzBoZUlMAAAAIAAAA1Ryb1JPAAAAJAAAA3RkZURFAAAAOgAA A5hpdElUAAAALgAAA9JzdlNFAAAALgAABAB6aENOAAAAEAAABC5qYUpQAAAAFgAABD5lbEdSAAAAJAAA BFRwdFBPAAAAOAAABHhubE5MAAAAKgAABLBlc0VTAAAAKAAABNp0aFRIAAAAJAAABQJ0clRSAAAAIgAA BSZmaUZJAAAALAAABUhockhSAAAAOgAABXRwbFBMAAAANgAABa5ydVJVAAAAJgAABeRhckVHAAAAKAAA BgpkYURLAAAANAAABjIAVgFhAGUAbwBiAGUAYwBuAP0AIABzAGkAdgD9ACAAcAByAG8AZgBpAGwARwBl AG4AZQByAGkAYwAgAEcAcgBhAHkAIABQAHIAbwBmAGkAbABlAFAAZQByAGYAaQBsACAAZABlACAAZwBy AGkAcwAgAGcAZQBuAOgAcgBpAGMAQx6lAHUAIABoAOwAbgBoACAATQDgAHUAIAB4AOEAbQAgAEMAaAB1 AG4AZwBQAGUAcgBmAGkAbAAgAEMAaQBuAHoAYQAgAEcAZQBuAOkAcgBpAGMAbwQXBDAEMwQwBDsETAQ9 BDgEOQAgBD8EQAQ+BEQEMAQ5BDsAIABHAHIAYQB5AFAAcgBvAGYAaQBsACAAZwDpAG4A6QByAGkAcQB1 AGUAIABnAHIAaQBzAMEAbAB0AGEAbADhAG4AbwBzACAAcwB6APwAcgBrAGUAIABwAHIAbwBmAGkAbJAa dShwcJaOgnJfaWPPj/AARwBlAG4AZQByAGkAcwBrACAAZwByAOUAdABvAG4AZQBwAHIAbwBmAGkAbMd8 vBgAIABHAHIAYQB5ACDVBLhc0wzHfABPAGIAZQBjAG4A/QAgAWEAZQBkAP0AIABwAHIAbwBmAGkAbAXk BegF1QXkBdkF3AAgAEcAcgBhAHkAIAXbBdwF3AXZAFAAcgBvAGYAaQBsACAAZwByAGkAIABnAGUAbgBl AHIAaQBjAEEAbABsAGcAZQBtAGUAaQBuAGUAcwAgAEcAcgBhAHUAcwB0AHUAZgBlAG4ALQBQAHIAbwBm AGkAbABQAHIAbwBmAGkAbABvACAAZwByAGkAZwBpAG8AIABnAGUAbgBlAHIAaQBjAG8ARwBlAG4AZQBy AGkAcwBrACAAZwByAOUAcwBrAGEAbABlAHAAcgBvAGYAaQBsZm6QGnBwXqZjz4/wZYdO9k4AgiwwsDDs MKQw1zDtMNUwoTCkMOsDkwO1A70DuQO6A8wAIAPAA8EDvwPGA68DuwAgA7MDugPBA7kAUABlAHIAZgBp AGwAIABnAGUAbgDpAHIAaQBjAG8AIABkAGUAIABjAGkAbgB6AGUAbgB0AG8AcwBBAGwAZwBlAG0AZQBl AG4AIABnAHIAaQBqAHMAcAByAG8AZgBpAGUAbABQAGUAcgBmAGkAbAAgAGcAcgBpAHMAIABnAGUAbgDp AHIAaQBjAG8OQg4bDiMORA4fDiUOTA4qDjUOQA4XDjIOFw4xDkgOJw5EDhsARwBlAG4AZQBsACAARwBy AGkAIABQAHIAbwBmAGkAbABpAFkAbABlAGkAbgBlAG4AIABoAGEAcgBtAGEAYQBwAHIAbwBmAGkAaQBs AGkARwBlAG4AZQByAGkBDQBrAGkAIABwAHIAbwBmAGkAbAAgAHMAaQB2AGkAaAAgAHQAbwBuAG8AdgBh AFUAbgBpAHcAZQByAHMAYQBsAG4AeQAgAHAAcgBvAGYAaQBsACAAcwB6AGEAcgBvAVsAYwBpBB4EMQRJ BDgEOQAgBEEENQRABEsEOQAgBD8EQAQ+BEQEOAQ7BEwGRQZEBkEAIAYqBjkGMQZKBkEAIABHAHIAYQB5 ACAGJwZEBjkGJwZFAEcAZQBuAGUAcgBlAGwAIABnAHIA5QB0AG8AbgBlAGIAZQBzAGsAcgBpAHYAZQBs AHMAZQAAdGV4dAAAAABDb3B5cmlnaHQgMjAwNyBBcHBsZSBJbmMuLCBhbGwgcmlnaHRzIHJlc2VydmVk LgBYWVogAAAAAAAA81EAAQAAAAEWzGN1cnYAAAAAAAAAAQHNAADSLS4vMFokY2xhc3NuYW1lWCRjbGFz c2VzXxAQTlNCaXRtYXBJbWFnZVJlcKMvMTJaTlNJbWFnZVJlcFhOU09iamVjdNItLjQ1V05TQXJyYXmi NDLSLS43OF5OU011dGFibGVBcnJheaM3NDLTOjsOPD0+V05TV2hpdGVcTlNDb2xvclNwYWNlRDAgMAAQ A4AM0i0uQEFXTlNDb2xvcqJAMtItLkNEV05TSW1hZ2WiQzIACAARABoAJAApADIANwBJAEwAUQBTAGIA aAB3AH4AhQCSAJkApQCtALwAvgDAAMUAxwDJAMsAzQDUANkA5ADmAOgA6gDvAPIA9AD2APgA/wEWATIB NAE2Cf4KAwoOChcKKgouCjkKQgpHCk8KUgpXCmYKagpxCnkKhgqLCo0KjwqUCpwKnwqkCqwAAAAAAAAC AQAAAAAAAABFAAAAAAAAAAAAAAAAAAAKrw ================================================ FILE: BGMApp/BGMApp/Images.xcassets/AirPlayIcon.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "AirPlay.pdf", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "16x16", "idiom" : "mac", "filename" : "appicon_16.png", "scale" : "1x" }, { "size" : "16x16", "idiom" : "mac", "filename" : "appicon_32.png", "scale" : "2x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "appicon_32.png", "scale" : "1x" }, { "size" : "32x32", "idiom" : "mac", "filename" : "appicon_64.png", "scale" : "2x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "appicon_128.png", "scale" : "1x" }, { "size" : "128x128", "idiom" : "mac", "filename" : "appicon_256.png", "scale" : "2x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "appicon_256.png", "scale" : "1x" }, { "size" : "256x256", "idiom" : "mac", "filename" : "appicon_512.png", "scale" : "2x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "appicon_512.png", "scale" : "1x" }, { "size" : "512x512", "idiom" : "mac", "filename" : "appicon_1024.png", "scale" : "2x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/FermataIcon.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "FermataIcon.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/Volume0.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "Volume0.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/Volume1.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "Volume1.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/Volume2.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "Volume2.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Images.xcassets/Volume3.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "filename" : "Volume3.pdf" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: BGMApp/BGMApp/Info.plist ================================================ AudioHardwarePowerHint None CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 0.4.3 CFBundleSignature ???? CFBundleVersion 1.0.0 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSUIElement NSAppleEventsUsageDescription Background Music needs to control your music player app if you want it to automatically pause your music. NSAppleScriptEnabled NSHumanReadableCopyright Copyright © 2016-2024 Background Music contributors NSMainNibFile MainMenu NSMicrophoneUsageDescription The "Background Music" virtual audio device sends system audio to Background Music (the app) through a virtual input device. NSPrincipalClass NSApplication NSServices OSAScriptingDefinition BGMApp.sdef NSSupportsAutomaticGraphicsSwitching ================================================ FILE: BGMApp/BGMApp/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: BGMApp/BGMApp/Music Players/BGMDecibel.h ================================================ // 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 . // // BGMDecibel.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" @interface BGMDecibel : BGMMusicPlayerBase @end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMDecibel.m ================================================ // 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 . // // BGMDecibel.m // BGMApp // // Copyright © 2016-2018 Kyle Neideck // Copyright © 2016 Tanner Hoke // // Self Include #import "BGMDecibel.h" // Auto-generated Scripting Bridge header #import "Decibel.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMDecibel { BGMScriptingBridge* scriptingBridge; } - (instancetype) init { if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"A9790CD5-4886-47C7-9FFC-DD70743CF2BF"] name:@"Decibel" bundleID:@"org.sbooth.Decibel"])) { scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self]; } return self; } - (DecibelApplication* __nullable) decibel { return (DecibelApplication* __nullable)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { return self.decibel.running; } - (BOOL) isPlaying { return self.running && self.decibel.playing; } - (BOOL) isPaused { // We don't want to return true when Decibel is stopped, rather than paused. At least for me, Decibel // returns -1 for playbackTime and playbackPosition when it's neither playing nor paused. BOOL probablyNotStopped = self.decibel.playbackTime >= 0 || self.decibel.playbackPosition >= 0; return self.running && !self.decibel.playing && probablyNotStopped; } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMDecibel::pause: Pausing Decibel"); [self.decibel pause]; } return wasPlaying; } - (BOOL) unpause { // isPaused checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPaused = self.paused; if (wasPaused) { DebugMsg("BGMDecibel::unpause: Unpausing Decibel"); [self.decibel play]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMGooglePlayMusicDesktopPlayer.h ================================================ // 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 . // // BGMGooglePlayMusicDesktopPlayer.h // BGMApp // // Copyright © 2019 Kyle Neideck // // We have a lot more code for GPMDP than most music players largely because GPMDP has a WebSockets // API and because the user has to enter a code from GPMDP to allow BGMApp to control it. // Currently, the other music players all have AppleScript APIs, so for them the OS asks the user // for permission on our behalf automatically and handles the whole process for us. // // This class implements the usual BGMMusicPlayer methods and handles the UI for authenticating // with GPMDP. BGMGooglePlayMusicDesktopPlayerConnection manages the connection to GPMDP and hides // the details of its API. // // Superclass/Protocol Import #import "BGMMusicPlayer.h" #pragma clang assume_nonnull begin API_AVAILABLE(macos(10.10)) @interface BGMGooglePlayMusicDesktopPlayer : BGMMusicPlayerBase + (NSArray>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMGooglePlayMusicDesktopPlayer.m ================================================ // 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 . // // BGMGooglePlayMusicDesktopPlayer.m // BGMApp // // Copyright © 2019 Kyle Neideck // // Self Include #import "BGMGooglePlayMusicDesktopPlayer.h" // Local Includes #import "BGM_Types.h" #import "BGM_Utils.h" #import "BGMAppWatcher.h" #import "BGMGooglePlayMusicDesktopPlayerConnection.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMGooglePlayMusicDesktopPlayer { BGMUserDefaults* userDefaults; BGMGooglePlayMusicDesktopPlayerConnection* connection; BGMAppWatcher* appWatcher; // True while the auth code dialog is open. The user types in the four-digit auth code from // GPMDP when we connect to it for the first time. BOOL showingAuthCodeDialog; // True if the user has cancelled the auth code dialog. We only show the auth code dialog again // after the user has changed the music player and then changed it back to GPMDP (or restarted // BGMApp). BOOL authCancelled; } + (NSArray>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults { return @[[[self alloc] initWithUserDefaults:userDefaults]]; } - (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults { // If you're copying this class, replace the ID string with a new one generated by uuidgen (the // command line tool). NSUUID* playerID = [BGMMusicPlayerBase makeID:@"FCDCC01F-4BF1-4AD2-BE3E-6B7659A90A3F"]; if ((self = [super initWithMusicPlayerID:playerID name:@"GPMDP" toolTip:@"Google Play Music Desktop Player" bundleID:@"google-play-music-desktop-player"])) { userDefaults = defaults; showingAuthCodeDialog = NO; authCancelled = NO; // We don't strictly need to use a weak ref (at least not yet), but it doesn't hurt. BGMGooglePlayMusicDesktopPlayer* __weak weakSelf = self; connection = [[BGMGooglePlayMusicDesktopPlayerConnection alloc] initWithUserDefaults:userDefaults authRequiredHandler:^{ BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf; return [strongSelf requestAuthCodeFromUser]; } connectionErrorHandler:^{ BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf; [strongSelf showConnectionErrorDialog]; } apiVersionMismatchHandler:^(NSString* reportedAPIVersion) { BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf; [strongSelf showAPIVersionMismatchDialog:reportedAPIVersion]; }]; // Set up callbacks that run when GPMDP is opened or closed. appWatcher = [[BGMAppWatcher alloc] initWithBundleID:BGMNN(self.bundleID) appLaunched:^{ BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf; [strongSelf gpmdpWasLaunched]; } appTerminated:^{ BGMGooglePlayMusicDesktopPlayer* strongSelf = weakSelf; [strongSelf gpmdpWasTerminated]; }]; } return self; } - (void) gpmdpWasLaunched { if (self.selected) { // Reconnect so we can control GPMDP. DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasLaunched: GPMDP launched. Connecting"); // Try up to 10 times because GPMDP won't start accepting connections until it's finished // starting up. // // TODO: If GPMDP shows an alert before it finishes launching, it doesn't start accepting // connections until the alert is dismissed, which can make this can timeout. // TODO: Is the error dialog still shown if the user closes GPMDP again while we're // retrying? It shouldn't be. [connection connectWithRetries:10]; } } - (void) gpmdpWasTerminated { if (self.selected) { // Allow the connection to clean up and reset itself. DebugMsg("BGMGooglePlayMusicDesktopPlayer::gpmdpWasTerminated: GPMDP has been closed."); [connection disconnect]; } } - (void) wasSelected { [super wasSelected]; // Allow the auth code dialog to be shown again if we were hiding it because the user cancelled // it last time. authCancelled = NO; if (self.running) { // Only retry once so the error message is shown fairly quickly if we fail to connect. [connection connectWithRetries:1]; } } - (void) wasDeselected { [super wasDeselected]; [connection disconnect]; } - (NSString* __nullable) requestAuthCodeFromUser { if (showingAuthCodeDialog) { DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: " "Already showing the auth code dialog"); return nil; } if (authCancelled) { DebugMsg("BGMGooglePlayMusicDesktopPlayer::requestAuthCodeFromUser: " "Previously cancelled. Doing nothing."); return nil; } showingAuthCodeDialog = YES; // Ask the user to read the auth code from GPMDP and type it in to BGMApp. NSString* __nullable authCode = [self showAuthCodeDialog]; showingAuthCodeDialog = NO; return authCode; } - (NSString* __nullable) showAuthCodeDialog { // When this isn't being called because the user just changed something in BGMApp (e.g. GPMDP // was closed, they selected it in BGMApp for the first time, then opened GPMDP later), we could // use notifications instead of an NSAlert. But it probably wouldn't happen often enough to be // worth the effort. NSAlert* alert = [NSAlert new]; alert.messageText = @"Background Music needs permission to control GPMDP."; alert.informativeText = @"It should be displaying a four-digit code for you to enter."; [alert addButtonWithTitle:@"OK"]; [alert addButtonWithTitle:@"Cancel"]; // The text field to type the auth code in. // TODO: Can we derive these dimensions from something instead of hardcoding them? NSTextField* input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 50, 24)]; [alert setAccessoryView:input]; // Focus the text field (so the user doesn't have to do it themselves). [alert.window setInitialFirstResponder:input]; // Bring GMPDP to the front, underneath our NSAlert, so the user can see the auth code. [self showGPMDPBehindAuthCodeDialog]; NSModalResponse buttonPressed = [alert runModal]; if (buttonPressed == NSAlertFirstButtonReturn) { // Set input's value to the text entered by the user so we can access it. [input validateEditing]; DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: Got auth code: "); return input.stringValue; } else { DebugMsg("BGMGooglePlayMusicDesktopPlayer::showAuthCodeDialog: " "The user cancelled the auth code dialog"); authCancelled = YES; return nil; } } - (void) showGPMDPBehindAuthCodeDialog { // Dispatched because if we do this just before showing the auth code dialog, the user's current // active window will be deactivated, the auth code dialog will become the active window and // macOS will act as if the user activated it themselves. To avoid stealing key focus, it won't // activate GPMDP. // // We could pass NSApplicationActivateIgnoringOtherApps to activateWithOptions instead, but then // GPMDP would be activated even if the user really did activate a different application, which // would steal focus from it. // // 250 ms is a reasonable value on my system, but won't always be long enough. When it isn't, // GPMDP won't be activated, but that just means the user will have to do it themselves. const int64_t delay = 250 * NSEC_PER_MSEC; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_main_queue(), ^{ // Make GMPDP the frontmost app. NSArray* gpmdpApps = [NSRunningApplication runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)]; if (gpmdpApps.count > 0) { [gpmdpApps[0] activateWithOptions:0]; } // Focus the auth code dialog. It will already be in front of GPMDP because // it's modal. Dispatched for the same reason as above. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay), dispatch_get_main_queue(), ^{ [NSApp activateIgnoringOtherApps:YES]; }); }); } - (void) showConnectionErrorDialog { NSString* errorMsg = @"Could not connect to Google Play Music Desktop Player"; NSString* troubleshootingMsg = [NSString stringWithFormat: @"Make sure \"Enable JSON API\" and \"Enable Playback API\" are both checked in GPMDP's " "settings, then restart GPMDP.\n\n" "GPMDP should be listening on its default port, 5672.\n\n" "Consider filing a bug report at %s", kBGMIssueTrackerURL]; [self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg]; } - (void) showAPIVersionMismatchDialog:(NSString*)reportedAPIVersion { NSString* errorMsg = @"Google Play Music Desktop Player Version Not Supported"; NSString* troubleshootingMsg = [NSString stringWithFormat: @"GPMDP reported its API version as \"%@\", which Background Music doesn't support " "yet. Background Music might not be able to control GPMDP properly.\n\n" "Feel free to open an issue about this at %s", reportedAPIVersion, kBGMIssueTrackerURL]; [self showErrorDialog:errorMsg troubleshootingMsg:troubleshootingMsg]; } - (void) showErrorDialog:(NSString*)errorMsg troubleshootingMsg:(NSString*)troubleshootingMsg { if (!self.running) { // GPMDP isn't running, so there's no need to inform the user. (The "Auto-pause GPMDP" menu // item will be greyed out, but that's handled elsewhere.) DebugMsg("BGMGooglePlayMusicDesktopPlayer::showErrorDialog: Not running"); return; } NSLog(@"%@", errorMsg); // Show the error in a UI dialog. NSAlert* alert = [NSAlert new]; alert.messageText = errorMsg; alert.informativeText = troubleshootingMsg; // TODO: Show the suppression checkbox and save its value in user defaults. alert.showsSuppressionButton = NO; [alert addButtonWithTitle:@"OK"]; [alert runModal]; } - (BOOL) isRunning { // We have to check with NSRunningApplication instead of just setting a flag in appWatcher's // callbacks because BGMAutoPauseMenuItem calls this method when it's notified by its own // instance of BGMAppWatcher. If BGMAutoPauseMenuItem got notified first, the flag wouldn't be // updated in time. // // At some point we might want to try to avoid this by making the BGMMusicPlayers' running // properties observable. NSArray* instances = [NSRunningApplication runningApplicationsWithBundleIdentifier:BGMNN(self.bundleID)]; return instances.count > 0; } - (BOOL) isPlaying { return self.running && connection.playing; } - (BOOL) isPaused { return self.running && connection.paused; } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here. BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMGooglePlayMusicDesktopPlayer::pause: Pausing Google Play Music Desktop " "Player"); // There's a race condition here and in unpause because, if the user paused GPMDP just // before we called playPause, GPMDP would play instead of pausing. I'm not sure there's // much we can/should do about it. [connection playPause]; } return wasPlaying; } - (BOOL) unpause { // isPaused checks isRunning, so we don't need to check it here. BOOL wasPaused = self.paused; if (wasPaused) { DebugMsg("BGMGooglePlayMusicDesktopPlayer::unpause: Unpausing Google Play Music Desktop " "Player"); [connection playPause]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMGooglePlayMusicDesktopPlayerConnection.h ================================================ // 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 . // // BGMGooglePlayMusicDesktopPlayerConnection.h // BGMApp // // Copyright © 2019 Kyle Neideck // // Local Includes #import "BGMUserDefaults.h" // System Includes #import #import #pragma clang assume_nonnull begin API_AVAILABLE(macos(10.10)) @interface BGMGooglePlayMusicDesktopPlayerConnection : NSObject // authRequiredHandler: A UI callback that asks the user for the auth code GPMDP will display. // Returns the auth code they entered, or nil. // connectionErrorHandler: A UI callback that shows a connection error message. // apiVersionMismatchHandler: A UI callback that shows a warning dialog explaining that GPMDP // reported an API version that we don't support yet. - (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults authRequiredHandler:(NSString* __nullable (^)(void))authHandler connectionErrorHandler:(void (^)(void))errorHandler apiVersionMismatchHandler:(void (^)(NSString* reportedAPIVersion))apiVersionHandler; // Returns before the connection has been fully established. The playing and paused properties will // remain false until the connection is complete, but playPause can be called at any time after // calling this method. // // If the connection fails, it will be retried after a one second delay, up to the number of times // given. - (void) connectWithRetries:(int)retries; - (void) disconnect; // Tell GPMDP to play if it's paused or pause if it's playing. - (void) playPause; @property (readonly) BOOL playing; @property (readonly) BOOL paused; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMGooglePlayMusicDesktopPlayerConnection.m ================================================ // 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 . // // BGMGooglePlayMusicDesktopPlayerConnection.m // BGMApp // // Copyright © 2019 Kyle Neideck // // Self Include #import "BGMGooglePlayMusicDesktopPlayerConnection.h" // Local Includes #import "BGM_Utils.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin // When GooglePlayMusicDesktopPlayer.js sends a message to this class, it sets the message handler // name to one of these, which tells us what type of message it is. (This is a macro because you // can't make a static const NSArray.) #define kScriptMessageHandlerNames (@[@"gpmdp", @"log", @"error"]) @implementation BGMGooglePlayMusicDesktopPlayerConnection { // GPMDP has a WebSocket API, so we use a WKWebView to access it using Javascript. Using a // proper library would make the code a bit cleaner and save a little memory, but I'm not sure // it would be worth adding an external dependency for that. WKWebView* webView; NSString* __nullable permanentAuthCode; BGMUserDefaults* userDefaults; // The number of times to retry if we fail to connect. For example, if GPMDP is still starting // up. Set to 0 when we aren't trying to connect. int connectionRetries; // A UI callback that asks the user for the auth code GPMDP will display. NSString* __nullable (^authRequiredHandler)(void); // A UI callback that shows a connection error message. void (^connectionErrorHandler)(void); // A UI callback that shows a warning dialog explaining that GPMDP reported an API version that // we don't support yet. void (^apiVersionMismatchHandler)(NSString* reportedAPIVersion); } - (instancetype) initWithUserDefaults:(BGMUserDefaults*)defaults authRequiredHandler:(NSString* __nullable (^)(void))authHandler connectionErrorHandler:(void (^)(void))errorHandler apiVersionMismatchHandler:(void (^)(NSString* reportedAPIVersion))apiVersionHandler { if((self = [super init])) { userDefaults = defaults; authRequiredHandler = authHandler; connectionErrorHandler = errorHandler; apiVersionMismatchHandler = apiVersionHandler; connectionRetries = 0; // Lazily initialised. permanentAuthCode = nil; // Report that GPMDP is stopped until we know otherwise. _playing = NO; _paused = NO; } return self; } // Creates and initialises webView, a WKWebView we use to communicate with GPMDP over WebSockets. - (void) createWebView { // Read the Javascript we'll need for this. NSString* __nullable jsPath = [[NSBundle mainBundle] pathForResource:@"GooglePlayMusicDesktopPlayer.js" ofType:nil]; NSError* err; NSString* __nullable jsStr = (!jsPath ? nil : [NSString stringWithContentsOfFile:BGMNN(jsPath) encoding:NSUTF8StringEncoding error:&err]); if (err || !jsStr || [jsStr isEqualToString:@""]) { // TODO: Return an error so the caller can show an error dialog or something. NSLog(@"Error loading GPMDP Javascript file: %@", err); } else { webView = [WKWebView new]; // Register to receive messages from our Javascript. The messages are handled in // userContentController. We register several times using different names as a convenient // way to separate messages from GPMDP, messages to log and errors. for (NSString* name in kScriptMessageHandlerNames) { [webView.configuration.userContentController addScriptMessageHandler:self name:name]; } // Load our Javascript functions into webView so we can call them later. [self evaluateJavaScript:BGMNN(jsStr)]; } } - (void) connectWithRetries:(int)retries { if (retries < 0) { BGMAssert(false, "retries < 0"); return; } if (!permanentAuthCode) { // Read the API auth code from user defaults (actually the keychain), if there is one. If // the user hasn't authenticated before, it will be nil. // // We do this lazily because it can show a password dialog in debug/unsigned builds. permanentAuthCode = userDefaults.googlePlayMusicDesktopPlayerPermanentAuthCode; } connectionRetries = retries; // Create the WKWebView we'll use to connect to GPMDP with WebSockets. Using a WKWebView means // Background Music uses a bit more memory while connected to GPMDP, around 15 MB for me, but // saves us having to complicate the build process to add a dependency on a proper library. [self createWebView]; if (permanentAuthCode) { DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::connectWithRetries: " "Connecting with auth code"); NSString* __nullable percentEncodedCode = [BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:BGMNN(permanentAuthCode)]; [self evaluateJavaScript:[NSString stringWithFormat:@"connect('%@');", percentEncodedCode]]; } else { DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::connectWithRetries: " "Connecting without auth code"); [self evaluateJavaScript:@"connect();"]; } // Check whether GPMDP is playing, paused or stopped. [self requestPlaybackState]; } - (void) disconnect { // Stop retrying if we're in the process of connecting. connectionRetries = 0; // evaluateJavaScript is only safe to call on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::disconnect: Disconnecting"); [webView evaluateJavaScript:@"disconnect();" completionHandler:^(id __nullable result, NSError* __nullable error) { #pragma unused (result) if (error) { NSLog(@"Error closing connection to GPMDP: %@", error); } // Allow the WKWebView to be garbage collected. for (NSString* name in kScriptMessageHandlerNames) { [webView.configuration.userContentController removeScriptMessageHandlerForName:name]; } webView = nil; }]; }); } - (void) evaluateJavaScript:(NSString*)js { // evaluateJavaScript is only safe to call on the main thread. dispatch_async(dispatch_get_main_queue(), ^{ [webView evaluateJavaScript:js completionHandler:^(id __nullable result, NSError* __nullable error) { #pragma unused (result) if (error) { // TODO: We should probably show an error dialog in some cases. NSLog(@"JS error: %@", error); } }]; }); } - (void) playPause { [self evaluateJavaScript:@"playPause();"]; } - (void) sendAuthCode:(NSString*)authCode { // Don't log the code itself just in case it could be a security problem. DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::sendAuthCode: Sending GPMDP auth code"); // Percent-encode the user input just in case they entered something that could execute as // Javascript. We could limit the input to four digits instead, but this should be fine. NSString* __nullable percentEncodedCode = [BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:authCode]; // We send the message to GPMDP even if percentEncodedCode is nil so it will reply with an error // and BGMApp will ask the user for the auth code again. NSString* js = [NSString stringWithFormat:@"window.sendAuthCode('%@');", percentEncodedCode]; [self evaluateJavaScript:js]; } - (void) sendPermanentAuthCode { NSString* __nullable code = permanentAuthCode; if (code) { // Don't log the code itself just in case it could be a security problem. DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::sendPermanentAuthCode: " "Sending GPMDP permanent auth code"); // Percent-encode it just in case something it includes could be executed as Javascript. NSString* __nullable percentEncodedCode = [BGMGooglePlayMusicDesktopPlayerConnection toPercentEncoded:BGMNN(code)]; // Pass the code to our WKWebView so it can send it to GPMDP. NSString* js = [NSString stringWithFormat:@"sendPermanentAuthCode('%@');", percentEncodedCode]; [self evaluateJavaScript:js]; } else { NSLog(@"BGMGooglePlayMusicDesktopPlayerConnection::sendPermanentAuthCode: No code to send"); } } + (NSString* __nullable)toPercentEncoded:(NSString*)rawString { // Just percent-encode every character (by passing an empty NSCharacterSet as the allowed // characters). NSString* __nullable percentEncoded = [rawString stringByAddingPercentEncodingWithAllowedCharacters: [NSCharacterSet characterSetWithCharactersInString:@""]]; if (percentEncoded) { return percentEncoded; } else { // The docs say that stringByAddingPercentEncodingWithAllowedCharacters returns nil "if the // transformation is not possible", but don't explain when that could happen. According to // https://stackoverflow.com/a/33558934/1091063 it can be caused by the string containing // invalid unicode. NSLog(@"Could not encode"); return nil; } } // Ask GPMDP whether it's playing, paused or stopped. The response is handled asynchronously in // handleResultMessage. - (void) requestPlaybackState { [self evaluateJavaScript:@"requestPlaybackState();"]; } #pragma mark WKScriptMessageHandler Methods - (void) userContentController:(WKUserContentController*)userContentController didReceiveScriptMessage:(WKScriptMessage*)message { #pragma unused (userContentController) if ([@"log" isEqual:message.name]) { // The message body is always a string in this case. [self handleLogMessage:message.body]; } else if ([@"error" isEqual:message.name]) { [self handleConnectionError]; } else { BGMAssert([@"gpmdp" isEqual:message.name], "Unexpected message handler name"); [self handleGPMDPMessage:message]; } } - (void) handleLogMessage:(NSString*)message { (void)message; #if DEBUG if (permanentAuthCode) { // Avoid logging the auth code, which would be a minor security issue. message = [message stringByReplacingOccurrencesOfString:BGMNN(permanentAuthCode) withString:@""]; } DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::userContentController: %s", message.UTF8String); #endif } - (void) handleConnectionError { if (connectionRetries > 0) { DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleConnectionError: " "Retrying in 1 second"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // Check connectionRetries again because disconnect may have been called. if (connectionRetries > 0) { DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::" "handleConnectionError: Retrying"); [self connectWithRetries:(connectionRetries - 1)]; } }); } else { NSLog(@"BGMGooglePlayMusicDesktopPlayerConnection::handleConnectionError: " "No retries left. Giving up."); connectionErrorHandler(); } } - (void) handleGPMDPMessage:(WKScriptMessage*)message { // See https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/docs/PlaybackAPI_WebSocket.md // Type check. if (![message.body isKindOfClass:[NSDictionary class]]) { NSLog(@"Unexpected message body type"); return; } NSDictionary* body = message.body; NSString* messageType; // The key for the message type is "channel", except when the message is a response, in which // case the key can be "namespace". if ([body[@"channel"] isKindOfClass:[NSString class]]) { messageType = body[@"channel"]; } else if ([body[@"namespace"] isKindOfClass:[NSString class]]) { messageType = body[@"namespace"]; } else { NSLog(@"No channel/namespace"); return; } // Handle the message depending on its type (or ignore it). if ([@"API_VERSION" isEqual:messageType]) { [self handleAPIVersionMessage:body]; } else if ([@"connect" isEqual:messageType]) { [self handleConnectMessage:body]; } else if ([@"playState" isEqual:messageType]) { [self handlePlayStateMessage:body]; } else if ([@"result" isEqual:messageType]) { [self handleResultMessage:body]; } } - (void) handleAPIVersionMessage:(NSDictionary*)body { DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: Response: %s", [NSString stringWithFormat:@"%@", body].UTF8String); // Type check. if (![body[@"payload"] isKindOfClass:[NSString class]]) { NSLog(@"Unexpected payload type"); [self handleConnectionError]; return; } NSString* apiVersion = body[@"payload"]; // "1.0.0" -> ["1", "0", "0"] NSArray* versionParts = [apiVersion componentsSeparatedByString:@"."]; // Check the major version number is 1, which is the only major version we support. if (versionParts.count > 0) { NSInteger majorVersion = versionParts[0].integerValue; DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: " "Major version: %lu", majorVersion); if (majorVersion == 1) { // GPMDP uses SemVer, so as long as the major version number matches what we can handle, // it should work. DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: " "This API version is supported"); return; } } // Show a warning dialog box to the user, but try to continue anyway. There's probably a // reasonable chance it'll still work. DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleAPIVersionMessage: " "Unsupported GPMDP API version"); apiVersionMismatchHandler(apiVersion); } - (void) handleConnectMessage:(NSDictionary*)body { // Don't log the response as it may contain the auth code. DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleConnectMessage: Received response"); // Type check. if (![body[@"payload"] isKindOfClass:[NSString class]]) { NSLog(@"Unexpected payload type"); [self handleConnectionError]; return; } NSString* payload = body[@"payload"]; if ([@"CODE_REQUIRED" isEqual:payload]) { // Ask the user for the auth code GPMDP is displaying and send it to GPMDP to finish // connecting. NSString* __nullable authCode = authRequiredHandler(); if (authCode) { [self sendAuthCode:BGMNN(authCode)]; } } else { // The payload should be the permanent auth code. permanentAuthCode = payload; [self sendPermanentAuthCode]; // Save the code to the keychain so we can use it when connecting to GPMDP in future. userDefaults.googlePlayMusicDesktopPlayerPermanentAuthCode = permanentAuthCode; } } - (void) handlePlayStateMessage:(NSDictionary*)body { (void)body; DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handlePlayStateMessage: Response: %s", [NSString stringWithFormat:@"%@", body].UTF8String); // This message tells us the playstate has changed, but doesn't differentiate between stopped // and paused. The response to this API request will. See handleResultMessage. // TODO: Can it transition from stopped to paused? Would that be a problem? [self requestPlaybackState]; } - (void) handleResultMessage:(NSDictionary*)body { DebugMsg("BGMGooglePlayMusicDesktopPlayerConnection::handleResultMessage: Response: %s", [NSString stringWithFormat:@"%@", body].UTF8String); // Type check. if (![body[@"value"] isKindOfClass:[NSNumber class]]) { NSLog(@"No value"); return; } // 0 - Playback is stopped // 1 - Track is paused // 2 - Track is playing int state = ((NSNumber*)body[@"value"]).intValue; _playing = (state == 2); _paused = (state == 1); } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMHermes.h ================================================ // 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 . // // BGMHermes.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" @interface BGMHermes : BGMMusicPlayerBase @end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMHermes.m ================================================ // 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 . // // BGMHermes.m // BGMApp // // Copyright © 2016-2018 Kyle Neideck // // Self Include #import "BGMHermes.h" // Auto-generated Scripting Bridge header #import "Hermes.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMHermes { BGMScriptingBridge* scriptingBridge; } - (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] initWithMusicPlayer:self]; } return self; } - (HermesApplication* __nullable) hermes { return (HermesApplication* __nullable)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { // Note that this will return NO if is self.hermes is nil (i.e. Hermes isn't running). return self.hermes.running; } // isPlaying and isPaused check self.running first just in case Hermes is closed but self.hermes hasn't become // nil yet. In that case, reading self.hermes.playerState could make Scripting Bridge open Hermes. - (BOOL) isPlaying { return self.running && (self.hermes.playbackState == HermesPlayerStatesPlaying); } - (BOOL) isPaused { return self.running && (self.hermes.playbackState == HermesPlayerStatesPaused); } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMHermes::pause: Pausing Hermes"); [self.hermes pause]; } return wasPlaying; } - (BOOL) unpause { // isPaused checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPaused = self.paused; if (wasPaused) { DebugMsg("BGMHermes::unpause: Unpausing Hermes"); [self.hermes play]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMMusic.h ================================================ // 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 . // // BGMMusic.h // BGMApp // // Copyright © 2016, 2019 Kyle Neideck // Copyright © 2019 theLMGN // // Superclass/Protocol Import #import "BGMMusicPlayer.h" #pragma clang assume_nonnull begin @interface BGMMusic : BGMMusicPlayerBase @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMMusic.m ================================================ // 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 . // // BGMMusic.m // BGMApp // // Copyright © 2016-2019 Kyle Neideck, theLMGN // // Self Include #import "BGMMusic.h" // Auto-generated Scripting Bridge header #import "Music.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMMusic { BGMScriptingBridge* scriptingBridge; } + (NSUUID*) sharedMusicPlayerID { NSUUID* __nullable musicPlayerID = [[NSUUID alloc] initWithUUIDString:@"829B8069-8BD2-481D-BD40-54AB8CDAE228"]; NSAssert(musicPlayerID, @"BGMMusic::sharedMusicPlayerID: !musicPlayerID"); return (NSUUID*)musicPlayerID; } - (instancetype) init { if ((self = [super initWithMusicPlayerID:[BGMMusic sharedMusicPlayerID] name:@"Music" bundleID:@"com.apple.Music"])) { scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self]; } return self; } - (MusicApplication* __nullable) music { return (MusicApplication*)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { return self.music.running; } // isPlaying and isPaused check self.running first just in case Music is closed but self.music // hasn't become nil yet. In that case, reading self.music.playerState could make Scripting Bridge // open Music. - (BOOL) isPlaying { return self.running && (self.music.playerState == MusicEPlSPlaying); } - (BOOL) isPaused { return self.running && (self.music.playerState == MusicEPlSPaused); } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMMusic::pause: Pausing Music"); [self.music pause]; } return wasPlaying; } - (BOOL) unpause { // isPaused checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPaused = self.paused; if (wasPaused) { DebugMsg("BGMMusic::unpause: Unpausing Music"); [self.music playpause]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMMusicPlayer.h ================================================ // 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 . // // BGMMusicPlayer.h // BGMApp // // Copyright © 2016, 2018, 2019 Kyle Neideck // // The base classes and protocol for objects that represent a music player app. // // To add support for a music player, create a class that implements the BGMMusicPlayer protocol // and add it to initWithAudioDevices in BGMMusicPlayers.mm. // // You'll probably want to subclass BGMMusicPlayerBase and, if the music player supports // AppleScript, use BGMScriptingBridge. Your class might need to override the icon method if the // default implementation from BGMMusicPlayerBase doesn't work. // // BGMSpotify will probably be the most useful example to follow, but they're all pretty // similar. The music player classes written so far all use Scripting Bridge to communicate with // the music player apps (see iTunes.h/Spotify.h) but any other way is fine too. // // BGMDriver will use either the music player's bundle ID or PID to match it to the audio it // plays. (Though using PIDs hasn't been tested yet.) // // If you're not sure what bundle ID the music player uses, install a debug build of BGMDriver // and play something in the music player. The easiest way is to do // build_and_install.sh -d // BGMDriver will log the bundle ID to system.log when it becomes aware of the music player. // // Local Includes #import "BGMUserDefaults.h" // System Includes #import #pragma clang assume_nonnull begin @protocol BGMMusicPlayer // Classes return an instance of themselves for each music player app they make available in // BGMApp. So far that's always been a single instance, but that will probably change eventually. // Most classes don't need to override the default implementation from BGMMusicPlayerBase. // // But, for example, a class for custom music players would probably return an instance for each // custom player the user has created. (Also note that it could return an empty array.) // // TODO: I think the return type should actually be NSArray*, but that doesn't seem // to work. There's a Clang bug about this: https://llvm.org/bugs/show_bug.cgi?id=27323 // (though it hasn't been confirmed yet). + (NSArray>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults; // We need a unique ID for each music player to store in user defaults. In the most common case, // classes that provide a static (or at least bounded) number of music players, you can generate // IDs with uuidgen (the command line tool) and include them in your class as constants. Otherwise, // you'll probably want to store them in user defaults and load them in createInstancesWithDefaults. @property (readonly) NSUUID* musicPlayerID; // The name, tool-tip and icon of the music player, to be used in the UI. @property (readonly) NSString* name; @property (readonly) NSString* __nullable toolTip; @property (readonly) NSImage* __nullable icon; @property (readonly) NSString* __nullable bundleID; // Classes will usually ignore this property and leave it nil unless the music player has no // bundle ID. // // TODO: If we ever add a music player class that uses this property, it'll need a way to inform // BGMDevice of changes. It might be easiest to have BGMMusicPlayers to observe this property, // on the selected music player, with KVO and update BGMDevice when it changes. Or // BGMMusicPlayers could pass a pointer to itself to createInstancesWithDefaults. @property NSNumber* __nullable pid; // True if this is currently the selected music player. @property (readonly) BOOL selected; // The state of the music player. // // True if the music player app is open. @property (readonly, getter=isRunning) BOOL running; // True if the music player is playing a song or some other user-selected audio file. Note that // the music player playing audio for UI, notifications, etc. won't make this true (which is why we // need this property and can't just ask BGMDriver if the music player is playing audio). @property (readonly, getter=isPlaying) BOOL playing; // True if the music player has a current/open song (or whatever) and will continue playing it if // BGMMusicPlayer::unpause is called. Normally because the user was playing a song and they or // BGMApp paused it. @property (readonly, getter=isPaused) BOOL paused; // Called when the user selects this music player. - (void) wasSelected; // Called when this was the selected music player and the user just selected a different one. - (void) wasDeselected; // Pause the music player. Does nothing if the music player is already paused or isn't running. // Returns YES if the music player is paused now but wasn't before, returns NO otherwise. - (BOOL) pause; // Unpause the music player. Does nothing if the music player is already playing or isn't running. // Returns YES if the music player is playing now but wasn't before, returns NO otherwise. - (BOOL) unpause; @end @interface BGMMusicPlayerBase : NSObject - (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID name:(NSString*)name bundleID:(NSString* __nullable)bundleID; - (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID name:(NSString*)name toolTip:(NSString*)toolTip bundleID:(NSString* __nullable)bundleID; - (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID name:(NSString*)name toolTip:(NSString* __nullable)toolTip bundleID:(NSString* __nullable)bundleID pid:(NSNumber* __nullable)pid; // Convenience wrapper around NSUUID's initWithUUIDString. musicPlayerIDString must be a string // generated by uuidgen (command line tool), e.g. "60BA9739-B6DD-4E6A-8134-51410A45BB84". + (NSUUID*) makeID:(NSString*)musicPlayerIDString; // BGMMusicPlayer default implementations + (NSArray>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults; @property (readonly) NSImage* __nullable icon; @property (readonly) NSUUID* musicPlayerID; @property (readonly) NSString* name; @property (readonly) NSString* __nullable toolTip; @property (readonly) NSString* __nullable bundleID; @property NSNumber* __nullable pid; @property (readonly) BOOL selected; - (void) wasSelected; - (void) wasDeselected; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMMusicPlayer.m ================================================ // 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 . // // BGMMusicPlayer.m // BGMApp // // Copyright © 2016-2019 Kyle Neideck // // Self Include #import "BGMMusicPlayer.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMMusicPlayerBase @synthesize musicPlayerID = _musicPlayerID; @synthesize name = _name; @synthesize toolTip = _toolTip; @synthesize bundleID = _bundleID; @synthesize pid = _pid; @synthesize selected = _selected; - (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID name:(NSString*)name bundleID:(NSString* __nullable)bundleID { return [self initWithMusicPlayerID:musicPlayerID name:name toolTip:nil bundleID:bundleID pid:nil]; } - (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID name:(NSString*)name toolTip:(NSString*)toolTip bundleID:(NSString* __nullable)bundleID { return [self initWithMusicPlayerID:musicPlayerID name:name toolTip:toolTip bundleID:bundleID pid:nil]; } - (instancetype) initWithMusicPlayerID:(NSUUID*)musicPlayerID name:(NSString*)name toolTip:(NSString* __nullable)toolTip bundleID:(NSString* __nullable)bundleID pid:(NSNumber* __nullable)pid { if ((self = [super init])) { NSAssert(musicPlayerID, @"BGMMusicPlayerBase::initWithMusicPlayerID: !musicPlayerID"); NSAssert([self conformsToProtocol:@protocol(BGMMusicPlayer)], @"BGMMusicPlayerBase::initWithMusicPlayerID: !conformsToProtocol"); _musicPlayerID = musicPlayerID; _name = name; _toolTip = toolTip; _bundleID = bundleID; _pid = pid; _selected = NO; } return self; } + (NSUUID*) makeID:(NSString*)musicPlayerIDString { NSUUID* __nullable musicPlayerID = [[NSUUID alloc] initWithUUIDString:musicPlayerIDString]; NSAssert(musicPlayerID, @"BGMMusicPlayerBase::makeID: !musicPlayerID"); return (NSUUID*)musicPlayerID; } #pragma mark BGMMusicPlayer default implementations + (NSArray>*) createInstancesWithDefaults:(BGMUserDefaults*)userDefaults { #pragma unused (userDefaults) // TODO: Fix this warning properly. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-literal-conversion" return @[ [self new] ]; #pragma clang diagnostic pop } - (NSImage* __nullable) icon { NSString* __nullable bundleID = self.bundleID; NSString* __nullable bundlePath = (!bundleID ? nil : [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:(NSString*)bundleID]); return (!bundlePath ? nil : [[NSWorkspace sharedWorkspace] iconForFile:(NSString*)bundlePath]); } - (void) wasSelected { _selected = YES; } - (void) wasDeselected { _selected = NO; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMMusicPlayers.h ================================================ // 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 . // // BGMMusicPlayers.h // BGMApp // // Copyright © 2016, 2019 Kyle Neideck // // Holds the music players (i.e. BGMMusicPlayer objects) available in BGMApp. Also keeps track of // which music player is currently selected by the user. // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMMusicPlayer.h" #import "BGMUserDefaults.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMMusicPlayers : NSObject // Calls initWithAudioDevices:musicPlayers: with sensible defaults. - (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices userDefaults:(BGMUserDefaults*)defaults; // defaultMusicPlayerID is the musicPlayerID (see BGMMusicPlayer.h) of the music player that should be // selected by default. // // The createInstancesWithDefaults method of each class in musicPlayerClasses will be called and // the results will be stored in the musicPlayers property. - (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices defaultMusicPlayerID:(NSUUID*)defaultMusicPlayerID musicPlayerClasses:(NSArray>*)musicPlayerClasses userDefaults:(BGMUserDefaults*)defaults; @property (readonly) NSArray>* musicPlayers; // The music player currently selected in the preferences menu. BGMDevice is informed when this property // is changed. @property id selectedMusicPlayer; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMMusicPlayers.mm ================================================ // 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 . // // BGMMusicPlayers.mm // BGMApp // // Copyright © 2016-2019 Kyle Neideck // // Self include #import "BGMMusicPlayers.h" // Local includes #import "BGM_Types.h" // Music player includes #import "BGMiTunes.h" #import "BGMSpotify.h" #import "BGMVLC.h" #import "BGMVOX.h" #import "BGMDecibel.h" #import "BGMHermes.h" #import "BGMSwinsian.h" #import "BGMMusic.h" #import "BGMGooglePlayMusicDesktopPlayer.h" #pragma clang assume_nonnull begin @implementation BGMMusicPlayers { BGMAudioDeviceManager* audioDevices; BGMUserDefaults* userDefaults; } @synthesize selectedMusicPlayer = _selectedMusicPlayer; - (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices userDefaults:(BGMUserDefaults*)defaults { // The classes handling each music player we support. If you write a new music player class, add // it to this array. NSArray>* mpClasses = @[ [BGMVOX class], [BGMVLC class], [BGMSpotify class], [BGMiTunes class], [BGMDecibel class], [BGMHermes class], [BGMSwinsian class], [BGMMusic class] ]; // We only support Google Play Music Desktop Player on macOS 10.10 and higher. if (@available(macOS 10.10, *)) { mpClasses = [mpClasses arrayByAddingObject:[BGMGooglePlayMusicDesktopPlayer class]]; } return [self initWithAudioDevices:devices defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID] musicPlayerClasses:mpClasses userDefaults:defaults]; } - (instancetype) initWithAudioDevices:(BGMAudioDeviceManager*)devices defaultMusicPlayerID:(NSUUID*)defaultMusicPlayerID musicPlayerClasses:(NSArray>*)musicPlayerClasses userDefaults:(BGMUserDefaults*)defaults { if ((self = [super init])) { audioDevices = devices; userDefaults = defaults; // Init _musicPlayers, an array containing one object for each music player in BGMApp. // // Each music player class has a factory method, createInstancesWithDefaults, that returns // all the instances of that class BGMApp will use. (Though so far it's always just one // instance.) NSMutableArray* musicPlayers = [NSMutableArray new]; for (Class musicPlayerClass in musicPlayerClasses) { NSArray>* instances = [musicPlayerClass createInstancesWithDefaults:userDefaults]; [musicPlayers addObjectsFromArray:instances]; } _musicPlayers = [NSArray arrayWithArray:musicPlayers]; // Set _selectedMusicPlayer to its setting from last time BGMApp ran. (Unless this is the first run or // that music player isn't available this time.) [self initSelectedMusicPlayerFromUserDefaults]; if (!_selectedMusicPlayer) { // Couldn't set _selectedMusicPlayer from user defaults, so try BGMDevice's music player property. [self initSelectedMusicPlayerFromBGMDevice]; } if (!_selectedMusicPlayer) { // The user hasn't changed the music player yet, so we set the default music player as selected. [self setSelectedMusicPlayerByID:defaultMusicPlayerID]; } NSAssert(_selectedMusicPlayer, @"BGMMusicPlayers::initWithAudioDevices: !_selectedMusicPlayer"); } return self; } - (void) initSelectedMusicPlayerFromUserDefaults { // Load the selected music player setting from user defaults. NSString* __nullable selectedMusicPlayerIDStr = userDefaults.selectedMusicPlayerID; NSUUID* __nullable selectedMusicPlayerID = nil; if (selectedMusicPlayerIDStr) { NSString* idStrNN = selectedMusicPlayerIDStr; selectedMusicPlayerID = [[NSUUID alloc] initWithUUIDString:idStrNN]; NSAssert(selectedMusicPlayerID, @"BGMMusicPlayers::initSelectedMusicPlayerFromUserDefaults: !selectedMusicPlayerID"); } if (selectedMusicPlayerID) { NSUUID* idNN = selectedMusicPlayerID; BOOL didChangeMusicPlayer = [self setSelectedMusicPlayerByID:idNN]; #if DEBUG DebugMsg("BGMMusicPlayers::initSelectedMusicPlayerFromUserDefaults: %s selectedMusicPlayerIDStr=%s", (didChangeMusicPlayer ? "Selected music player restored from user defaults." : "The selected music player setting found in user defaults didn't match an available music player."), selectedMusicPlayerIDStr.UTF8String); #else #pragma unused (didChangeMusicPlayer) #endif } } - (void) initSelectedMusicPlayerFromBGMDevice { // When the selected music player setting hasn't been stored in user defaults yet, we get the music player // bundle ID from the driver and look for the music player with that bundle ID. This is mainly done for // backwards compatibility. NSString* __nullable bundleID = (__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)")); if (bundleID && ![bundleID isEqualToString:@""]) { // Find any music players with a bundle ID matching the one from BGMDriver. NSArray>* matchingMusicPlayers = @[ ]; for (id musicPlayer in _musicPlayers) { NSString* bundleIDNN = bundleID; if ([musicPlayer.bundleID isEqualToString:bundleIDNN]) { DebugMsg("BGMMusicPlayers::initSelectedMusicPlayerFromBGMDevice: Bundle ID on BGMDevice matches %s", musicPlayer.name.UTF8String); matchingMusicPlayers = [matchingMusicPlayers arrayByAddingObject:musicPlayer]; } } // Currently, the music players all have different bundle IDs, but that might change at some point. We // might want to consider some websites as music players, for example. So we don't change the setting // unless the bundle ID only matches one music player. if (matchingMusicPlayers.count == 1) { // (Use setSelectedMusicPlayerImpl to avoid setSelectedMusicPlayer being called in init.) [self setSelectedMusicPlayerImpl:matchingMusicPlayers[0]]; } } } - (id) selectedMusicPlayer { return _selectedMusicPlayer; } - (void) setSelectedMusicPlayer:(id)newSelectedMusicPlayer { // Apparently you shouldn't call properties' setter methods in init (KVO notifications might trigger, etc.) // so the actual work is done in setSelectedMusicPlayerImpl. [self setSelectedMusicPlayerImpl:newSelectedMusicPlayer]; NSAssert(self.selectedMusicPlayer == newSelectedMusicPlayer, @"BGMMusicPlayers::setSelectedMusicPlayer: selectedMusicPlayer wasn't set to the object expected"); } - (BOOL) setSelectedMusicPlayerByID:(NSUUID*)newSelectedMusicPlayerID { id __nullable newSelectedMusicPlayer = nil; // Find the music player with the given ID, if there is one. for (id musicPlayer in _musicPlayers) { if ([musicPlayer.musicPlayerID isEqual:newSelectedMusicPlayerID]) { NSAssert(!newSelectedMusicPlayer, @"BGMMusicPlayers::setSelectedMusicPlayerByID: Non-unique musicPlayerID"); newSelectedMusicPlayer = musicPlayer; } } if (newSelectedMusicPlayer) { // (Use setSelectedMusicPlayerImpl to avoid setSelectedMusicPlayer being called in init.) id newPlayerNN = newSelectedMusicPlayer; [self setSelectedMusicPlayerImpl:newPlayerNN]; return YES; } else { return NO; } } - (void) setSelectedMusicPlayerImpl:(id)newSelectedMusicPlayer { NSAssert([_musicPlayers containsObject:newSelectedMusicPlayer], @"BGMMusicPlayers::setSelectedMusicPlayerImpl: Only the music players in the musicPlayers array can be selected. " "newSelectedMusicPlayer=%@", newSelectedMusicPlayer.name); if (_selectedMusicPlayer == newSelectedMusicPlayer) { DebugMsg("BGMMusicPlayers::setSelectedMusicPlayerImpl: %s is already the selected music " "player.", _selectedMusicPlayer.name.UTF8String); return; } // Tell the current music player (object) a different player has been selected. [_selectedMusicPlayer wasDeselected]; _selectedMusicPlayer = newSelectedMusicPlayer; DebugMsg("BGMMusicPlayers::setSelectedMusicPlayerImpl: Set selected music player to %s", _selectedMusicPlayer.name.UTF8String); // Update the selected music player on the driver. [self updateBGMDeviceMusicPlayerProperties]; // Save the new setting in user defaults. userDefaults.selectedMusicPlayerID = _selectedMusicPlayer.musicPlayerID.UUIDString; // Tell the music player (object) it's been selected. [_selectedMusicPlayer wasSelected]; } - (void) updateBGMDeviceMusicPlayerProperties { // Send the music player's PID and/or bundle ID to the driver. NSAssert(self.selectedMusicPlayer.pid || self.selectedMusicPlayer.bundleID, @"BGMMusicPlayers::updateBGMDeviceMusicPlayerProperties: Music player has neither bundle ID nor PID"); if (self.selectedMusicPlayer.pid) { [audioDevices bgmDevice].SetMusicPlayerProcessID((__bridge CFNumberRef)self.selectedMusicPlayer.pid); } if (self.selectedMusicPlayer.bundleID) { [audioDevices bgmDevice].SetMusicPlayerBundleID((__bridge CFStringRef)self.selectedMusicPlayer.bundleID); } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMScriptingBridge.h ================================================ // 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 . // // BGMScriptingBridge.h // BGMApp // // Copyright © 2016, 2018 Kyle Neideck // // A wrapper around Scripting Bridge's SBApplication that tries to avoid ever launching the application. // // We use Scripting Bridge to communicate with music player apps, which we never want to launch // ourselves. But creating an SBApplication for an app, or sending messages/events to an existing one, // can launch the app. // // As a workaround, this class has an SBApplication property, application (see below), which is nil // unless the music player app is running. That way messages sent while the app is closed are ignored. // // Local Includes #import "BGMMusicPlayer.h" // System Includes #import #import #pragma clang assume_nonnull begin @interface BGMScriptingBridge : NSObject // Only keeps a weak ref to musicPlayer. - (instancetype) initWithMusicPlayer:(id)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 // specifically say that returning nil is allowed. #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wnullability" - (id __nullable) eventDidFail:(const AppleEvent*)event withError:(NSError*)error; #pragma clang diagnostic pop @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMScriptingBridge.m ================================================ // 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 . // // BGMScriptingBridge.m // BGMApp // // Copyright © 2016-2019 Kyle Neideck // // Self Include #import "BGMScriptingBridge.h" // Local Includes #import "BGM_Utils.h" #import "BGMAppWatcher.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMScriptingBridge { id __weak _musicPlayer; BGMAppWatcher* appWatcher; } @synthesize application = _application; - (instancetype) initWithMusicPlayer:(id)musicPlayer { if ((self = [super init])) { _musicPlayer = musicPlayer; [self initApplication]; } return self; } - (void) initApplication { NSString* bundleID = _musicPlayer.bundleID; BGMAssert(bundleID, "Music players need a bundle ID to use ScriptingBridge"); BGMScriptingBridge* __weak weakSelf = self; void (^createSBApplication)(void) = ^{ BGMScriptingBridge* strongSelf = weakSelf; strongSelf->_application = [SBApplication applicationWithBundleIdentifier:bundleID]; // TODO: The SBApplication will still keep a strong ref to this object, so we would have to // make a separate delegate object to avoid the retain cycle. Not currently a problem // because we only ever create instances that live forever. strongSelf->_application.delegate = strongSelf; }; // Add observers that create/destroy the SBApplication when the music player is launched/terminated. We // only create the SBApplication when the music player is open. If it isn't open, creating the // SBApplication or sending it events could launch the music player. Whether or not it does depends on // the music player, and possibly the version of the music player, so to be safe we assume they all do. // // From the docs for SBApplication's applicationWithBundleIdentifier method: // "For applications that declare themselves to have a dynamic scripting interface, this method will // launch the application if it is not already running." appWatcher = [[BGMAppWatcher alloc] initWithBundleID:bundleID appLaunched:^{ DebugMsg("BGMScriptingBridge::initApplication: %s launched", bundleID.UTF8String); createSBApplication(); [weakSelf ensurePermission]; } appTerminated:^{ BGMScriptingBridge* strongSelf = weakSelf; DebugMsg("BGMScriptingBridge::initApplication: %s terminated", bundleID.UTF8String); strongSelf->_application = nil; }]; // Create the SBApplication if the music player is already running. if ([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleID].count > 0) { createSBApplication(); } } - (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 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 #pragma clang diagnostic ignored "-Wnullability" // See explanation in the header file. - (id __nullable) eventDidFail:(const AppleEvent*)event withError:(NSError*)error { #pragma clang diagnostic pop // So far, this just logs the error. #if DEBUG NSString* vars = [NSString stringWithFormat:@"event='%4.4s' error=%@ application=%@", (char*)&(event->descriptorType), error, self.application]; DebugMsg("BGMScriptingBridge::eventDidFail: Apple event sent to %s failed. %s", _musicPlayer.bundleID.UTF8String, vars.UTF8String); #else #pragma unused (event, error) #endif return nil; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMSpotify.h ================================================ // 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 . // // BGMSpotify.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" @interface BGMSpotify : BGMMusicPlayerBase @end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMSpotify.m ================================================ // 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 . // // BGMSpotify.m // BGMApp // // 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 // // Self Include #import "BGMSpotify.h" // Auto-generated Scripting Bridge header #import "Spotify.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMSpotify { BGMScriptingBridge* scriptingBridge; } - (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] initWithMusicPlayer:self]; } return self; } - (SpotifyApplication* __nullable) spotify { return (SpotifyApplication* __nullable)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { // Note that this will return NO if is self.spotify is nil (i.e. Spotify isn't running). return self.spotify.running; } // isPlaying and isPaused check self.running first just in case Spotify is closed but self.spotify hasn't become // nil yet. In that case, reading self.spotify.playerState could make Scripting Bridge open Spotify. - (BOOL) isPlaying { return self.running && (self.spotify.playerState == SpotifyEPlSPlaying); } - (BOOL) isPaused { return self.running && (self.spotify.playerState == SpotifyEPlSPaused); } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMSpotify::pause: Pausing Spotify"); [self.spotify 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("BGMSpotify::unpause: Unpausing Spotify"); [self.spotify playpause]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMSwinsian.h ================================================ // 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 . // // BGMSwinsian.h // BGMApp // // Copyright © 2018 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" #pragma clang assume_nonnull begin @interface BGMSwinsian : BGMMusicPlayerBase @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMSwinsian.m ================================================ // 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 . // // BGMSwinsian.m // BGMApp // // Copyright © 2018 Kyle Neideck // // Self Include #import "BGMSwinsian.h" // Auto-generated Scripting Bridge header #import "Swinsian.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMSwinsian { BGMScriptingBridge* scriptingBridge; } - (instancetype) init { // If you're copying this class, replace the ID string with a new one generated by uuidgen (the // command line tool). NSUUID* musicPlayerID = [BGMMusicPlayerBase makeID:@"B74D18F6-DFF7-4D88-B719-429CFF98CFFA"]; if ((self = [super initWithMusicPlayerID:musicPlayerID name:@"Swinsian" bundleID:@"com.swinsian.Swinsian"])) { scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self]; } return self; } - (SwinsianApplication* __nullable) swinsian { return (SwinsianApplication* __nullable)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { // Note that this will return NO if is self.swinsian is nil (i.e. Swinsian isn't running). return self.swinsian.running; } // isPlaying and isPaused check self.running first just in case Swinsian is closed but self.swinsian // hasn't become nil yet. In that case, reading self.swinsian.playerState could make Scripting // Bridge open Swinsian. - (BOOL) isPlaying { return self.running && (self.swinsian.playerState == SwinsianPlayerStatePlaying); } - (BOOL) isPaused { return self.running && (self.swinsian.playerState == SwinsianPlayerStatePaused); } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event. BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMSwinsian::pause: Pausing Swinsian"); [self.swinsian pause]; } return wasPlaying; } - (BOOL) unpause { // isPaused checks isRunning, so we don't need to check it here and waste an Apple event. BOOL wasPaused = self.paused; if (wasPaused) { DebugMsg("BGMSwinsian::unpause: Unpausing Swinsian"); [self.swinsian play]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMVLC.h ================================================ // 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 . // // BGMVLC.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" @interface BGMVLC : BGMMusicPlayerBase @end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMVLC.m ================================================ // 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 . // // BGMVLC.m // BGMApp // // Copyright © 2016-2018 Kyle Neideck // Copyright (C) 2012 Peter Ljunglöf. All rights reserved. // // Self Include #import "BGMVLC.h" // Auto-generated Scripting Bridge header #import "VLC.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMVLC { BGMScriptingBridge* scriptingBridge; } - (instancetype) init { if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"5226F4B9-C740-4045-A273-4B8EABC0E8FC"] name:@"VLC" bundleID:@"org.videolan.vlc"])) { scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self]; } return self; } - (VLCApplication* __nullable) vlc { return (VLCApplication*)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { return self.vlc.running; } // isPlaying and isPaused check self.running first just in case VLC is closed but self.vlc hasn't become // nil yet. In that case, reading other properties of self.vlc could make Scripting Bridge open VLC. - (BOOL) isPlaying { return self.running && self.vlc.playing; } - (BOOL) isPaused { // VLC is paused if it has a file open but isn't playing it return self.running && (self.vlc.nameOfCurrentItem != nil) && !self.vlc.playing; } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMVLC::pause: Pausing VLC"); [BGMVLC togglePlay]; } 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("BGMVLC::unpause: Unpausing VLC"); [BGMVLC togglePlay]; } return wasPaused; } // This is from SubTTS's STVLCPlayer class: // https://github.com/heatherleaf/subtts-mac/blob/master/SubTTS/STVLCPlayer.m // // VLC's Scripting Bridge interface doesn't seem to have a cleaner way to do this. + (void) togglePlay { NSString* src = @"tell application id \"org.videolan.vlc\" to play"; NSAppleScript* script = [[NSAppleScript alloc] initWithSource:src]; [script executeAndReturnError:nil]; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMVOX.h ================================================ // 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 . // // BGMVOX.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" @interface BGMVOX : BGMMusicPlayerBase @end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMVOX.m ================================================ // 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 . // // BGMVOX.m // BGMApp // // Copyright © 2016-2018 Kyle Neideck // // Self Include #import "BGMVOX.h" // Auto-generated Scripting Bridge header #import "VOX.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMVOX { BGMScriptingBridge* scriptingBridge; } - (instancetype) init { if ((self = [super initWithMusicPlayerID:[BGMMusicPlayerBase makeID:@"26498C5D-C18B-4689-8B41-9DA91A78FFAD"] name:@"VOX" bundleID:@"com.coppertino.Vox"])) { scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self]; } return self; } - (VoxApplication* __nullable) vox { return (VoxApplication*)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { return self.vox.running; } // isPlaying and isPaused check self.running first just in case VOX is closed but self.vox hasn't become // nil yet. In that case, reading self.vox.playerState could make Scripting Bridge open VOX. // // VOX's comment for its playerState property says "playing = 1, paused = 0". - (BOOL) isPlaying { return self.running && (self.vox.playerState == 1); } - (BOOL) isPaused { return self.running && (self.vox.playerState == 0); } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMVOX::pause: Pausing VOX"); [self.vox 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("BGMVOX::unpause: Unpausing VOX"); [self.vox playpause]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMiTunes.h ================================================ // 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 . // // BGMiTunes.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Superclass/Protocol Import #import "BGMMusicPlayer.h" @interface BGMiTunes : BGMMusicPlayerBase // The music player ID (see BGMMusicPlayer.h) used by BGMiTunes instances. (Though BGMApp only ever creates one instance of // BGMiTunes, sharedMusicPlayerID is exposed so iTunes can be set as the default music player.) + (NSUUID*) sharedMusicPlayerID; @end ================================================ FILE: BGMApp/BGMApp/Music Players/BGMiTunes.m ================================================ // 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 . // // BGMiTunes.m // BGMApp // // Copyright © 2016-2018 Kyle Neideck // // Self Include #import "BGMiTunes.h" // Auto-generated Scripting Bridge header #import "iTunes.h" // Local Includes #import "BGMScriptingBridge.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMiTunes { BGMScriptingBridge* scriptingBridge; } + (NSUUID*) sharedMusicPlayerID { NSUUID* __nullable musicPlayerID = [[NSUUID alloc] initWithUUIDString:@"7B62B5BF-CF90-4938-84E3-F16DEDC3F608"]; NSAssert(musicPlayerID, @"BGMiTunes::sharedMusicPlayerID: !musicPlayerID"); return (NSUUID*)musicPlayerID; } - (instancetype) init { if ((self = [super initWithMusicPlayerID:[BGMiTunes sharedMusicPlayerID] name:@"iTunes" bundleID:@"com.apple.iTunes"])) { scriptingBridge = [[BGMScriptingBridge alloc] initWithMusicPlayer:self]; } return self; } - (iTunesApplication* __nullable) iTunes { return (iTunesApplication*)scriptingBridge.application; } - (void) wasSelected { [super wasSelected]; [scriptingBridge ensurePermission]; } - (BOOL) isRunning { return self.iTunes.running; } // isPlaying and isPaused check self.running first just in case iTunes is closed but self.iTunes hasn't become // nil yet. In that case, reading self.iTunes.playerState could make Scripting Bridge open iTunes. - (BOOL) isPlaying { return self.running && (self.iTunes.playerState == iTunesEPlSPlaying); } - (BOOL) isPaused { return self.running && (self.iTunes.playerState == iTunesEPlSPaused); } - (BOOL) pause { // isPlaying checks isRunning, so we don't need to check it here and waste an Apple event BOOL wasPlaying = self.playing; if (wasPlaying) { DebugMsg("BGMiTunes::pause: Pausing iTunes"); [self.iTunes 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("BGMiTunes::unpause: Unpausing iTunes"); [self.iTunes playpause]; } return wasPaused; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Music Players/Decibel.h ================================================ /* * Decibel.h * * Generated with * sdef /Applications/Decibel.app | sdp -fh --basename Decibel */ #import #import @class DecibelApplication, DecibelDocument, DecibelWindow, DecibelApplication, DecibelTrack; enum DecibelSaveOptions { DecibelSaveOptionsYes = 'yes ' /* Save the file. */, DecibelSaveOptionsNo = 'no ' /* Do not save the file. */, DecibelSaveOptionsAsk = 'ask ' /* Ask the user whether or not to save the file. */ }; typedef enum DecibelSaveOptions DecibelSaveOptions; enum DecibelPrintingErrorHandling { DecibelPrintingErrorHandlingStandard = 'lwst' /* Standard PostScript error handling */, DecibelPrintingErrorHandlingDetailed = 'lwdt' /* print a detailed report of PostScript errors */ }; typedef enum DecibelPrintingErrorHandling DecibelPrintingErrorHandling; enum DecibelShuffleMode { DecibelShuffleModeOff = 'off ' /* Off */, DecibelShuffleModeTrack = 'trck' /* Track */, DecibelShuffleModeAlbum = 'albm' /* Album */, DecibelShuffleModeArtist = 'arts' /* Artist */ }; typedef enum DecibelShuffleMode DecibelShuffleMode; enum DecibelRepeatMode { DecibelRepeatModeOff = 'off ' /* Off */, DecibelRepeatModeTrack = 'trck' /* Track */, DecibelRepeatModeAlbum = 'albm' /* Album */, DecibelRepeatModeArtist = 'arts' /* Artist */, DecibelRepeatModeAll = 'all ' /* All */ }; typedef enum DecibelRepeatMode DecibelRepeatMode; @protocol DecibelGenericMethods - (void) closeSaving:(DecibelSaveOptions)saving savingIn:(NSURL *)savingIn; // Close a document. - (void) saveIn:(NSURL *)in_ as:(id)as; // Save a document. - (void) printWithProperties:(NSDictionary *)withProperties printDialog:(BOOL)printDialog; // Print a document. - (void) delete; // Delete an object. - (void) duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy an object. - (void) moveTo:(SBObject *)to; // Move an object to a new location. @end /* * Standard Suite */ // The application's top-level scripting object. @interface DecibelApplication : SBApplication - (SBElementArray *) documents; - (SBElementArray *) windows; @property (copy, readonly) NSString *name; // The name of the application. @property (readonly) BOOL frontmost; // Is this the active application? @property (copy, readonly) NSString *version; // The version number of the application. - (id) open:(id)x; // Open a document. - (void) print:(id)x withProperties:(NSDictionary *)withProperties printDialog:(BOOL)printDialog; // Print a document. - (void) quitSaving:(DecibelSaveOptions)saving; // Quit the application. - (BOOL) exists:(id)x; // Verify that an object exists. - (void) play; // Begin audio playback - (void) pause; // Suspend audio playback - (void) stop; // Stop audio playback - (void) playPause; // Begin or suspend audio playback - (void) seekForward; // Seek forward three seconds - (void) seekBackward; // Seek backward three seconds - (void) playSelection; // Play the selected track, or the first track if more than one are selected - (void) playPreviousTrack; // Play the previous logical track in the playlist - (void) playNextTrack; // Play the next logical track in the playlist - (void) addFile:(NSURL *)x; // Add a file to the playlist - (void) playFile:(NSURL *)x; // Add a file to the playlist and play it - (void) playTrackAtIndex:(NSInteger)x; // Play a track in the playlist - (void) increaseDeviceVolume; // Increase the device volume - (void) decreaseDeviceVolume; // Decrease the device volume - (void) increaseDigitalVolume; // Increase the digital volume - (void) decreaseDigitalVolume; // Decrease the digital volume - (void) clearPlaylist; // Clear the playlist - (void) scramblePlaylist; // Scramble the playlist @end // A document. @interface DecibelDocument : SBObject @property (copy, readonly) NSString *name; // Its name. @property (readonly) BOOL modified; // Has it been modified since the last save? @property (copy, readonly) NSURL *file; // Its location on disk, if it has one. @end // A window. @interface DecibelWindow : SBObject @property (copy, readonly) NSString *name; // The title of the window. - (NSInteger) id; // The unique identifier of the window. @property NSInteger index; // The index of the window, ordered front to back. @property NSRect bounds; // The bounding rectangle of the window. @property (readonly) BOOL closeable; // Does the window have a close button? @property (readonly) BOOL miniaturizable; // Does the window have a minimize button? @property BOOL miniaturized; // Is the window minimized right now? @property (readonly) BOOL resizable; // Can the window be resized? @property BOOL visible; // Is the window visible right now? @property (readonly) BOOL zoomable; // Does the window have a zoom button? @property BOOL zoomed; // Is the window zoomed right now? @property (copy, readonly) DecibelDocument *document; // The document whose contents are displayed in the window. @end /* * Decibel Scripting Suite */ // The Decibel application class. @interface DecibelApplication (DecibelScriptingSuite) - (SBElementArray *) tracks; @property (readonly) BOOL playing; // Is the player currently playing? @property (readonly) BOOL shuffling; // Is the player currently shuffling? @property (readonly) BOOL repeating; // Is the player currently repeating? @property (copy, readonly) DecibelTrack *nowPlaying; // The track that is currently playing? @property double deviceVolume; // The current device volume @property double digitalVolume; // The current digital volume @property double playbackPosition; // The current playback position [0, 1] @property double playbackTime; // The current playback time in seconds @property (readonly) BOOL canPlay; // Is the player currently playing? @property (readonly) BOOL canPlayPreviousTrack; // Is the player currently playing? @property (readonly) BOOL canPlayNextTrack; // Is the player currently playing? @property (readonly) BOOL canAdjustDeviceVolume; // Can the device volume be adjusted? @property DecibelShuffleMode shuffleMode; // Player shuffle mode @property DecibelRepeatMode repeatMode; // Player repeat mode @property (copy, readonly) SBObject *currentPlaylist; // The current playlist @end // A track in the playlist @interface DecibelTrack : SBObject - (NSString *) id; // The track's ID @property (copy, readonly) NSURL *file; // The track's location @property (readonly) double duration; // The track's duration in seconds @property (readonly) double sampleRate; // The track's sample rate in Hz @property (readonly) NSInteger bitDepth; // The bit depth @property (readonly) NSInteger channels; // The track's channels @property (copy) NSString *title; // The track's title @property (copy) NSString *artist; // The track's artist @property (copy) NSString *albumTitle; // The track's album title @property (copy) NSString *albumArtist; // The track's album artist @property NSInteger trackNumber; // The track's track number @property NSInteger trackTotal; // The total number of tracks on the album @property NSInteger discNumber; // The disc number containing the track @property NSInteger discTotal; // The total number of discs (for multidisc albums) @property BOOL partOfACompilation; // Is the track part of a compilation? @property (copy) NSString *genre; // The track's genre @property (copy) NSString *composer; // The track's composer @property (copy) NSString *releaseDate; // The track's release date @property (copy) NSString *ISRC; // The track's ISRC @property (copy) id MCN; // The track's MCN - (void) playTrack; // Play a track in the playlist @end ================================================ FILE: BGMApp/BGMApp/Music Players/GooglePlayMusicDesktopPlayer.js ================================================ // 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 . // // GooglePlayMusicDesktopPlayer.js // BGMApp // // Copyright © 2019 Kyle Neideck // // The specification for GPMDP's API: // https://github.com/MarshallOfSound/Google-Play-Music-Desktop-Player-UNOFFICIAL-/blob/master/docs/PlaybackAPI_WebSocket.md try { window._log = msg => { window.webkit.messageHandlers.log.postMessage(msg); }; // Global JS error handler. window.onerror = (msg, url, line, col, error) => { let extra = !col ? '' : '\nColumn: ' + col; extra += !error ? '' : '\nError: ' + error; // TODO: I'm not sure this log message is ever actually useful. window._log('Error: ' + msg + '\nURL: ' + url + '\nLine: ' + line + extra); window.webkit.messageHandlers.error.postMessage(error); }; // Send a JSON message to GPMDP. // // If we're connecting, this function will return immediately and the message will be sent after we // finish connecting. Logs an error and returns if window.connect() hasn't been called yet. window._sendJSON = json => { if (window._wsPromise) { window._wsPromise.then(() => { window._sendJSONImmediate(json); }).catch(error => { // TODO: Is there anything else we can do? Retries? window._log('Error sending JSON: ' + JSON.stringify(error)); }); } else { window._log('Error: No WebSocket promise. Discarding JSON message: ' + JSON.stringify(json)); } }; // Send a JSON message to GPMDP, but don't wait if we're in the process of connecting. // // Logs an error and returns if window.connect() hasn't been called yet. The authCode param is // optional and only used to hide the code in log messages. window._sendJSONImmediate = (json, authCode) => { let jsonStr = JSON.stringify(json); let jsonStrSanitized = authCode ? jsonStr.replace(authCode, "") : jsonStr; if (window._ws) { window._log('Sending JSON: ' + jsonStrSanitized); window._ws.send(jsonStr); } else { window._log('Error: No WebSocket. Discarding JSON message: ' + jsonStrSanitized); } }; // permanentAuthCode is optional. If this is the first time they've selected GPMDP, we won't have a // permanent code yet. window.connect = permanentAuthCode => { // Reset the connection state. window._requestID = 1; // Close the existing connection if we're already connected. window.disconnect(); // Create the new connection. window._ws = new WebSocket('ws://localhost:5672'); window._ws.onmessage = event => { // Pass the message along to BGMGooglePlayMusicDesktopPlayerConnection. let reply = JSON.parse(event.data); window.webkit.messageHandlers.gpmdp.postMessage(reply); }; window._wsPromise = new Promise((resolve, reject) => { window._ws.onopen = () => { // Send GPMDP the initial connection message. if (permanentAuthCode) { window._log('Connecting with auth code'); window.sendPermanentAuthCode(permanentAuthCode); } else { // Since we don't have an auth code, it will display a four-digit code and reply // telling us to ask the user to type it into Background Music. window._log('Connecting without auth code'); window._sendJSONImmediate({ 'namespace': 'connect', 'method': 'connect', 'arguments': ['Background Music'] }); } }; window._ws.onerror = error => { // Report the error to BGMGooglePlayMusicDesktopPlayerConnection. window.webkit.messageHandlers.error.postMessage(error); // Reject the connection promise. reject(error); }; // Store the function that resolves this promise. We resolve it after we finish // authenticating. window._resolveConnectionPromise = resolve; }); }; // Close the connection to GPMDP. Does nothing if we aren't connected. window.disconnect = () => { if (window._ws) { window._log('Closing WebSocket'); window._ws.close(); window._ws = null; } }; // Send an authentication code to GPMDP. To send a four-digit code (i.e. one entered by the user), // call this directly. To send a permanent code received from GPMDP, use // window.sendPermanentAuthCode(). // // authCode should be percent-encoded. window.sendAuthCode = authCode => { // Percent-decode the auth code string. We pass it percent-encoded just to make sure nothing in // it accidentally gets executed as Javascript. authCode = window.decodeURIComponent(authCode); window._sendJSONImmediate({ 'namespace': 'connect', 'method': 'connect', 'arguments': ['Background Music', authCode] }, authCode); }; // Send a permanent authentication code, received from GPMDP previously, to GPMDP. window.sendPermanentAuthCode = permanentAuthCode => { window._log('Sending permanent auth code'); window.sendAuthCode(permanentAuthCode); // TODO: If the code is rejected, GPMDP will send us a connect message and we'll show the auth // code dialog, but accepting the promise here means some messages we send might get // ignored. window._resolveConnectionPromise(); }; // Ask GPMDP to send us its current playback state (playing, paused or stopped). window.requestPlaybackState = () => { window._sendJSON({ 'namespace': 'playback', 'method': 'getPlaybackState', // We don't send any other types of request, so the ID we send only needs to be unique. 'requestID': window._requestID++ }); }; // Tell GPMDP to toggle between playing and paused. window.playPause = () => { window._sendJSON({ 'namespace': 'playback', 'method': 'playPause' }); }; } catch (error) { window.webkit.messageHandlers.log.postMessage('Error: ' + JSON.stringify(error)); window.webkit.messageHandlers.log.postMessage(JSON.stringify(error.stack)); window.webkit.messageHandlers.error.postMessage(error); } // Return an empty string as returning some types can cause an error when this Javascript is loaded // into the WKWebView. "" ================================================ FILE: BGMApp/BGMApp/Music Players/Hermes.h ================================================ /* * Hermes.h * * Generated with * sdef /Applications/Hermes.app | sdp -fh --basename Hermes */ #import #import @class HermesApplication, HermesSong, HermesStation; // Legal player states enum HermesPlayerStates { HermesPlayerStatesStopped = 'stop' /* Player is stopped */, HermesPlayerStatesPlaying = 'play' /* Player is playing */, HermesPlayerStatesPaused = 'paus' /* Player is paused */ }; typedef enum HermesPlayerStates HermesPlayerStates; /* * Hermes Suite */ // The Pandora player. @interface HermesApplication : SBApplication - (SBElementArray *) stations; @property NSInteger playbackVolume; // The current playback volume (0–100). @property HermesPlayerStates playbackState; // The current playback state. @property (readonly) double playbackPosition; // The current song’s playback position, in seconds. @property (readonly) double currentSongDuration; // The duration (length) of the current song, in seconds. @property (copy) HermesStation *currentStation; // The currently selected Pandora station. @property (copy, readonly) HermesSong *currentSong; // The currently playing (or paused) Pandora song (WARNING: This is an invalid reference in current versions of Hermes; you must access the current song’s properties individually or as a group directly instead.) - (void) playpause; // Play the current song if it is paused; pause the current song if it is playing. - (void) pause; // Pause the currently playing song. - (void) play; // Resume playing the current song. - (void) nextSong; // Skip to the next song on the current station. - (void) thumbsUp; // Tell Pandora you like the current song. - (void) thumbsDown; // Tell Pandora you don’t like the current song. - (void) tiredOfSong; // Tell Pandora you’re tired of the current song. - (void) increaseVolume; // Increase the playback volume. - (void) decreaseVolume; // Decrease the playback volume. - (void) maximizeVolume; // Set the playback volume to its maximum level. - (void) mute; // Mutes playback, saving the current volume level. - (void) unmute; // Restores the volume to the level prior to muting. @end // A Pandora song (track). @interface HermesSong : SBObject @property (copy, readonly) NSString *title; // The song’s title. @property (copy, readonly) NSString *artist; // The song’s artist. @property (copy, readonly) NSString *album; // The song’s album. @property (copy, readonly) NSString *artworkURL; // An image URL for the album’s cover artwork. @property (readonly) NSInteger rating; // The song’s numeric rating. @property (copy, readonly) NSString *albumURL; // A Pandora URL for more information on the album. @property (copy, readonly) NSString *artistURL; // A Pandora URL for more information on the artist. @property (copy, readonly) NSString *trackURL; // A Pandora URL for more information on the track. @end // A Pandora station. @interface HermesStation : SBObject @property (copy, readonly) NSString *name; // The station’s name. @property (copy, readonly) NSString *stationID; // The station’s ID. @end ================================================ FILE: BGMApp/BGMApp/Music Players/Music.h ================================================ /* * Music.h * * Generated with * sdef /System/Applications/Music.app | sdp -fh --basename Music */ #import #import @class MusicApplication, MusicItem, MusicAirPlayDevice, MusicArtwork, MusicEncoder, MusicEQPreset, MusicPlaylist, MusicAudioCDPlaylist, MusicLibraryPlaylist, MusicRadioTunerPlaylist, MusicSource, MusicSubscriptionPlaylist, MusicTrack, MusicAudioCDTrack, MusicFileTrack, MusicSharedTrack, MusicURLTrack, MusicUserPlaylist, MusicFolderPlaylist, MusicVisual, MusicWindow, MusicBrowserWindow, MusicEQWindow, MusicMiniplayerWindow, MusicPlaylistWindow, MusicVideoWindow; enum MusicEKnd { MusicEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */, MusicEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */, MusicEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */ }; typedef enum MusicEKnd MusicEKnd; enum MusicEnum { MusicEnumStandard = 'lwst' /* Standard PostScript error handling */, MusicEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */ }; typedef enum MusicEnum MusicEnum; enum MusicEPlS { MusicEPlSStopped = 'kPSS', MusicEPlSPlaying = 'kPSP', MusicEPlSPaused = 'kPSp', MusicEPlSFastForwarding = 'kPSF', MusicEPlSRewinding = 'kPSR' }; typedef enum MusicEPlS MusicEPlS; enum MusicERpt { MusicERptOff = 'kRpO', MusicERptOne = 'kRp1', MusicERptAll = 'kAll' }; typedef enum MusicERpt MusicERpt; enum MusicEShM { MusicEShMSongs = 'kShS', MusicEShMAlbums = 'kShA', MusicEShMGroupings = 'kShG' }; typedef enum MusicEShM MusicEShM; enum MusicESrc { MusicESrcLibrary = 'kLib', MusicESrcIPod = 'kPod', MusicESrcAudioCD = 'kACD', MusicESrcMP3CD = 'kMCD', MusicESrcRadioTuner = 'kTun', MusicESrcSharedLibrary = 'kShd', MusicESrcITunesStore = 'kITS', MusicESrcUnknown = 'kUnk' }; typedef enum MusicESrc MusicESrc; enum MusicESrA { MusicESrAAlbums = 'kSrL' /* albums only */, MusicESrAAll = 'kAll' /* all text fields */, MusicESrAArtists = 'kSrR' /* artists only */, MusicESrAComposers = 'kSrC' /* composers only */, MusicESrADisplayed = 'kSrV' /* visible text fields */, MusicESrASongs = 'kSrS' /* song names only */ }; typedef enum MusicESrA MusicESrA; enum MusicESpK { MusicESpKNone = 'kNon', MusicESpKFolder = 'kSpF', MusicESpKGenius = 'kSpG', MusicESpKLibrary = 'kSpL', MusicESpKMusic = 'kSpZ', MusicESpKPurchasedMusic = 'kSpM' }; typedef enum MusicESpK MusicESpK; enum MusicEMdK { MusicEMdKSong = 'kMdS' /* music track */, MusicEMdKMusicVideo = 'kVdV' /* music video track */, MusicEMdKUnknown = 'kUnk' }; typedef enum MusicEMdK MusicEMdK; enum MusicERtK { MusicERtKUser = 'kRtU' /* user-specified rating */, MusicERtKComputed = 'kRtC' /* iTunes-computed rating */ }; typedef enum MusicERtK MusicERtK; enum MusicEAPD { MusicEAPDComputer = 'kAPC', MusicEAPDAirPortExpress = 'kAPX', MusicEAPDAppleTV = 'kAPT', MusicEAPDAirPlayDevice = 'kAPO', MusicEAPDBluetoothDevice = 'kAPB', MusicEAPDHomePod = 'kAPH', MusicEAPDUnknown = 'kAPU' }; typedef enum MusicEAPD MusicEAPD; enum MusicEClS { MusicEClSUnknown = 'kUnk', MusicEClSPurchased = 'kPur', MusicEClSMatched = 'kMat', MusicEClSUploaded = 'kUpl', MusicEClSIneligible = 'kRej', MusicEClSRemoved = 'kRem', MusicEClSError = 'kErr', MusicEClSDuplicate = 'kDup', MusicEClSSubscription = 'kSub', MusicEClSNoLongerAvailable = 'kRev', MusicEClSNotUploaded = 'kUpP' }; typedef enum MusicEClS MusicEClS; @protocol MusicGenericMethods - (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(MusicEKnd)kind theme:(NSString *)theme; // Print the specified object(s) - (void) close; // Close an object - (void) delete; // Delete an element from an object - (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s) - (BOOL) exists; // Verify if an object exists - (void) open; // Open the specified object(s) - (void) save; // Save the specified object(s) - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. - (void) select; // select the specified object(s) @end /* * iTunes Suite */ // The application program @interface MusicApplication : SBApplication - (SBElementArray *) AirPlayDevices; - (SBElementArray *) browserWindows; - (SBElementArray *) encoders; - (SBElementArray *) EQPresets; - (SBElementArray *) EQWindows; - (SBElementArray *) miniplayerWindows; - (SBElementArray *) playlists; - (SBElementArray *) playlistWindows; - (SBElementArray *) sources; - (SBElementArray *) tracks; - (SBElementArray *) videoWindows; - (SBElementArray *) visuals; - (SBElementArray *) windows; @property (readonly) BOOL AirPlayEnabled; // is AirPlay currently enabled? @property (readonly) BOOL converting; // is a track currently being converted? @property (copy) NSArray *currentAirPlayDevices; // the currently selected AirPlay device(s) @property (copy) MusicEncoder *currentEncoder; // the currently selected encoder (MP3, AIFF, WAV, etc.) @property (copy) MusicEQPreset *currentEQPreset; // the currently selected equalizer preset @property (copy, readonly) MusicPlaylist *currentPlaylist; // the playlist containing the currently targeted track @property (copy, readonly) NSString *currentStreamTitle; // the name of the current song in the playing stream (provided by streaming server) @property (copy, readonly) NSString *currentStreamURL; // the URL of the playing stream or streaming web site (provided by streaming server) @property (copy, readonly) MusicTrack *currentTrack; // the current targeted track @property (copy) MusicVisual *currentVisual; // the currently selected visual plug-in @property BOOL EQEnabled; // is the equalizer enabled? @property BOOL fixedIndexing; // true if all AppleScript track indices should be independent of the play order of the owning playlist. @property BOOL frontmost; // is iTunes the frontmost application? @property BOOL fullScreen; // are visuals displayed using the entire screen? @property (copy, readonly) NSString *name; // the name of the application @property BOOL mute; // has the sound output been muted? @property double playerPosition; // the player’s position within the currently playing track in seconds. @property (readonly) MusicEPlS playerState; // is iTunes stopped, paused, or playing? @property (copy, readonly) SBObject *selection; // the selection visible to the user @property BOOL shuffleEnabled; // are songs played in random order? @property MusicEShM shuffleMode; // the playback shuffle mode @property MusicERpt songRepeat; // the playback repeat mode @property NSInteger soundVolume; // the sound output volume (0 = minimum, 100 = maximum) @property (copy, readonly) NSString *version; // the version of iTunes @property BOOL visualsEnabled; // are visuals currently being displayed? - (void) printPrintDialog:(BOOL)printDialog withProperties:(NSDictionary *)withProperties kind:(MusicEKnd)kind theme:(NSString *)theme; // Print the specified object(s) - (void) run; // Run iTunes - (void) quit; // Quit iTunes - (MusicTrack *) add:(NSArray *)x to:(SBObject *)to; // add one or more files to a playlist - (void) backTrack; // reposition to beginning of current track or go to previous track if already at start of current track - (MusicTrack *) convert:(NSArray *)x; // convert one or more files or tracks - (void) fastForward; // skip forward in a playing track - (void) nextTrack; // advance to the next track in the current playlist - (void) pause; // pause playback - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. - (void) playpause; // toggle the playing/paused state of the current track - (void) previousTrack; // return to the previous track in the current playlist - (void) resume; // disable fast forward/rewind and resume playback, if playing. - (void) rewind; // skip backwards in a playing track - (void) stop; // stop playback - (void) openLocation:(NSString *)x; // Opens a Music Store or audio stream URL @end // an item @interface MusicItem : SBObject @property (copy, readonly) SBObject *container; // the container of the item - (NSInteger) id; // the id of the item @property (readonly) NSInteger index; // The index of the item in internal application order. @property (copy) NSString *name; // the name of the item @property (copy, readonly) NSString *persistentID; // the id of the item as a hexadecimal string. This id does not change over time. @property (copy) NSDictionary *properties; // every property of the item - (void) download; // download a cloud track or playlist - (void) reveal; // reveal and select a track or playlist @end // an AirPlay device @interface MusicAirPlayDevice : MusicItem @property (readonly) BOOL active; // is the device currently being played to? @property (readonly) BOOL available; // is the device currently available? @property (readonly) MusicEAPD kind; // the kind of the device @property (copy, readonly) NSString *networkAddress; // the network (MAC) address of the device - (BOOL) protected; // is the device password- or passcode-protected? @property BOOL selected; // is the device currently selected? @property (readonly) BOOL supportsAudio; // does the device support audio playback? @property (readonly) BOOL supportsVideo; // does the device support video playback? @property NSInteger soundVolume; // the output volume for the device (0 = minimum, 100 = maximum) @end // a piece of art within a track or playlist @interface MusicArtwork : MusicItem @property (copy) NSImage *data; // data for this artwork, in the form of a picture @property (copy) NSString *objectDescription; // description of artwork as a string @property (readonly) BOOL downloaded; // was this artwork downloaded by iTunes? @property (copy, readonly) NSNumber *format; // the data format for this piece of artwork @property NSInteger kind; // kind or purpose of this piece of artwork @property (copy) NSData *rawData; // data for this artwork, in original format @end // converts a track to a specific file format @interface MusicEncoder : MusicItem @property (copy, readonly) NSString *format; // the data format created by the encoder @end // equalizer preset configuration @interface MusicEQPreset : MusicItem @property double band1; // the equalizer 32 Hz band level (-12.0 dB to +12.0 dB) @property double band2; // the equalizer 64 Hz band level (-12.0 dB to +12.0 dB) @property double band3; // the equalizer 125 Hz band level (-12.0 dB to +12.0 dB) @property double band4; // the equalizer 250 Hz band level (-12.0 dB to +12.0 dB) @property double band5; // the equalizer 500 Hz band level (-12.0 dB to +12.0 dB) @property double band6; // the equalizer 1 kHz band level (-12.0 dB to +12.0 dB) @property double band7; // the equalizer 2 kHz band level (-12.0 dB to +12.0 dB) @property double band8; // the equalizer 4 kHz band level (-12.0 dB to +12.0 dB) @property double band9; // the equalizer 8 kHz band level (-12.0 dB to +12.0 dB) @property double band10; // the equalizer 16 kHz band level (-12.0 dB to +12.0 dB) @property (readonly) BOOL modifiable; // can this preset be modified? @property double preamp; // the equalizer preamp level (-12.0 dB to +12.0 dB) @property BOOL updateTracks; // should tracks which refer to this preset be updated when the preset is renamed or deleted? @end // a list of songs/streams @interface MusicPlaylist : MusicItem - (SBElementArray *) tracks; - (SBElementArray *) artworks; @property (copy) NSString *objectDescription; // the description of the playlist @property BOOL disliked; // is this playlist disliked? @property (readonly) NSInteger duration; // the total length of all songs (in seconds) @property (copy) NSString *name; // the name of the playlist @property BOOL loved; // is this playlist loved? @property (copy, readonly) MusicPlaylist *parent; // folder which contains this playlist (if any) @property (readonly) NSInteger size; // the total size of all songs (in bytes) @property (readonly) MusicESpK specialKind; // special playlist kind @property (copy, readonly) NSString *time; // the length of all songs in MM:SS format @property (readonly) BOOL visible; // is this playlist visible in the Source list? - (void) moveTo:(SBObject *)to; // Move playlist(s) to a new location - (MusicTrack *) searchFor:(NSString *)for_ only:(MusicESrA)only; // search a playlist for tracks matching the search string. Identical to entering search text in the Search field in iTunes. @end // a playlist representing an audio CD @interface MusicAudioCDPlaylist : MusicPlaylist - (SBElementArray *) audioCDTracks; @property (copy) NSString *artist; // the artist of the CD @property BOOL compilation; // is this CD a compilation album? @property (copy) NSString *composer; // the composer of the CD @property NSInteger discCount; // the total number of discs in this CD’s album @property NSInteger discNumber; // the index of this CD disc in the source album @property (copy) NSString *genre; // the genre of the CD @property NSInteger year; // the year the album was recorded/released @end // the master music library playlist @interface MusicLibraryPlaylist : MusicPlaylist - (SBElementArray *) fileTracks; - (SBElementArray *) URLTracks; - (SBElementArray *) sharedTracks; @end // the radio tuner playlist @interface MusicRadioTunerPlaylist : MusicPlaylist - (SBElementArray *) URLTracks; @end // a music source (music library, CD, device, etc.) @interface MusicSource : MusicItem - (SBElementArray *) audioCDPlaylists; - (SBElementArray *) libraryPlaylists; - (SBElementArray *) playlists; - (SBElementArray *) radioTunerPlaylists; - (SBElementArray *) subscriptionPlaylists; - (SBElementArray *) userPlaylists; @property (readonly) long long capacity; // the total size of the source if it has a fixed size @property (readonly) long long freeSpace; // the free space on the source if it has a fixed size @property (readonly) MusicESrc kind; @end // a subscription playlist from Apple Music @interface MusicSubscriptionPlaylist : MusicPlaylist - (SBElementArray *) fileTracks; - (SBElementArray *) URLTracks; @end // playable audio source @interface MusicTrack : MusicItem - (SBElementArray *) artworks; @property (copy) NSString *album; // the album name of the track @property (copy) NSString *albumArtist; // the album artist of the track @property BOOL albumDisliked; // is the album for this track disliked? @property BOOL albumLoved; // is the album for this track loved? @property NSInteger albumRating; // the rating of the album for this track (0 to 100) @property (readonly) MusicERtK albumRatingKind; // the rating kind of the album rating for this track @property (copy) NSString *artist; // the artist/source of the track @property (readonly) NSInteger bitRate; // the bit rate of the track (in kbps) @property double bookmark; // the bookmark time of the track in seconds @property BOOL bookmarkable; // is the playback position for this track remembered? @property NSInteger bpm; // the tempo of this track in beats per minute @property (copy) NSString *category; // the category of the track @property (readonly) MusicEClS cloudStatus; // the iCloud status of the track @property (copy) NSString *comment; // freeform notes about the track @property BOOL compilation; // is this track from a compilation album? @property (copy) NSString *composer; // the composer of the track @property (readonly) NSInteger databaseID; // the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data. @property (copy, readonly) NSDate *dateAdded; // the date the track was added to the playlist @property (copy) NSString *objectDescription; // the description of the track @property NSInteger discCount; // the total number of discs in the source album @property NSInteger discNumber; // the index of the disc containing this track on the source album @property BOOL disliked; // is this track disliked? @property (copy, readonly) NSString *downloaderAppleID; // the Apple ID of the person who downloaded this track @property (copy, readonly) NSString *downloaderName; // the name of the person who downloaded this track @property (readonly) double duration; // the length of the track in seconds @property BOOL enabled; // is this track checked for playback? @property (copy) NSString *episodeID; // the episode ID of the track @property NSInteger episodeNumber; // the episode number of the track @property (copy) NSString *EQ; // the name of the EQ preset of the track @property double finish; // the stop time of the track in seconds @property BOOL gapless; // is this track from a gapless album? @property (copy) NSString *genre; // the music/audio genre (category) of the track @property (copy) NSString *grouping; // the grouping (piece) of the track. Generally used to denote movements within a classical work. @property (copy, readonly) NSString *kind; // a text description of the track @property (copy) NSString *longDescription; @property BOOL loved; // is this track loved? @property (copy) NSString *lyrics; // the lyrics of the track @property MusicEMdK mediaKind; // the media kind of the track @property (copy, readonly) NSDate *modificationDate; // the modification date of the content of this track @property (copy) NSString *movement; // the movement name of the track @property NSInteger movementCount; // the total number of movements in the work @property NSInteger movementNumber; // the index of the movement in the work @property NSInteger playedCount; // number of times this track has been played @property (copy) NSDate *playedDate; // the date and time this track was last played @property (copy, readonly) NSString *purchaserAppleID; // the Apple ID of the person who purchased this track @property (copy, readonly) NSString *purchaserName; // the name of the person who purchased this track @property NSInteger rating; // the rating of this track (0 to 100) @property (readonly) MusicERtK ratingKind; // the rating kind of this track @property (copy, readonly) NSDate *releaseDate; // the release date of this track @property (readonly) NSInteger sampleRate; // the sample rate of the track (in Hz) @property NSInteger seasonNumber; // the season number of the track @property BOOL shufflable; // is this track included when shuffling? @property NSInteger skippedCount; // number of times this track has been skipped @property (copy) NSDate *skippedDate; // the date and time this track was last skipped @property (copy) NSString *show; // the show name of the track @property (copy) NSString *sortAlbum; // override string to use for the track when sorting by album @property (copy) NSString *sortArtist; // override string to use for the track when sorting by artist @property (copy) NSString *sortAlbumArtist; // override string to use for the track when sorting by album artist @property (copy) NSString *sortName; // override string to use for the track when sorting by name @property (copy) NSString *sortComposer; // override string to use for the track when sorting by composer @property (copy) NSString *sortShow; // override string to use for the track when sorting by show name @property (readonly) long long size; // the size of the track (in bytes) @property double start; // the start time of the track in seconds @property (copy, readonly) NSString *time; // the length of the track in MM:SS format @property NSInteger trackCount; // the total number of tracks on the source album @property NSInteger trackNumber; // the index of the track on the source album @property BOOL unplayed; // is this track unplayed? @property NSInteger volumeAdjustment; // relative volume adjustment of the track (-100% to 100%) @property (copy) NSString *work; // the work name of the track @property NSInteger year; // the year the track was recorded/released @end // a track on an audio CD @interface MusicAudioCDTrack : MusicTrack @property (copy, readonly) NSURL *location; // the location of the file represented by this track @end // a track representing an audio file (MP3, AIFF, etc.) @interface MusicFileTrack : MusicTrack @property (copy) NSURL *location; // the location of the file represented by this track - (void) refresh; // update file track information from the current information in the track’s file @end // a track residing in a shared library @interface MusicSharedTrack : MusicTrack @end // a track representing a network stream @interface MusicURLTrack : MusicTrack @property (copy) NSString *address; // the URL for this track @end // custom playlists created by the user @interface MusicUserPlaylist : MusicPlaylist - (SBElementArray *) fileTracks; - (SBElementArray *) URLTracks; - (SBElementArray *) sharedTracks; @property BOOL shared; // is this playlist shared? @property (readonly) BOOL smart; // is this a Smart Playlist? @property (readonly) BOOL genius; // is this a Genius Playlist? @end // a folder that contains other playlists @interface MusicFolderPlaylist : MusicUserPlaylist @end // a visual plug-in @interface MusicVisual : MusicItem @end // any window @interface MusicWindow : MusicItem @property NSRect bounds; // the boundary rectangle for the window @property (readonly) BOOL closeable; // does the window have a close button? @property (readonly) BOOL collapseable; // does the window have a collapse button? @property BOOL collapsed; // is the window collapsed? @property BOOL fullScreen; // is the window full screen? @property NSPoint position; // the upper left position of the window @property (readonly) BOOL resizable; // is the window resizable? @property BOOL visible; // is the window visible? @property (readonly) BOOL zoomable; // is the window zoomable? @property BOOL zoomed; // is the window zoomed? @end // the main iTunes window @interface MusicBrowserWindow : MusicWindow @property (copy, readonly) SBObject *selection; // the selected songs @property (copy) MusicPlaylist *view; // the playlist currently displayed in the window @end // the iTunes equalizer window @interface MusicEQWindow : MusicWindow @end // the miniplayer window @interface MusicMiniplayerWindow : MusicWindow @end // a sub-window showing a single playlist @interface MusicPlaylistWindow : MusicWindow @property (copy, readonly) SBObject *selection; // the selected songs @property (copy, readonly) MusicPlaylist *view; // the playlist displayed in the window @end // the video window @interface MusicVideoWindow : MusicWindow @end ================================================ FILE: BGMApp/BGMApp/Music Players/Spotify.h ================================================ /* * Spotify.h * * Generated with * sdef /Applications/Spotify.app | sdp -fh --basename Spotify */ #import #import @class SpotifyApplication, SpotifyTrack, SpotifyApplication; enum SpotifyEPlS { SpotifyEPlSStopped = 'kPSS', SpotifyEPlSPlaying = 'kPSP', SpotifyEPlSPaused = 'kPSp' }; typedef enum SpotifyEPlS SpotifyEPlS; /* * Spotify Suite */ // The Spotify application. @interface SpotifyApplication : SBApplication @property (copy, readonly) SpotifyTrack *currentTrack; // The current playing track. @property NSInteger soundVolume; // The sound output volume (0 = minimum, 100 = maximum) @property (readonly) SpotifyEPlS playerState; // Is Spotify stopped, paused, or playing? @property double playerPosition; // The player’s position within the currently playing track in seconds. @property (readonly) BOOL repeatingEnabled; // Is repeating enabled in the current playback context? @property BOOL repeating; // Is repeating on or off? @property (readonly) BOOL shufflingEnabled; // Is shuffling enabled in the current playback context? @property BOOL shuffling; // Is shuffling on or off? - (void) nextTrack; // Skip to the next track. - (void) previousTrack; // Skip to the previous track. - (void) playpause; // Toggle play/pause. - (void) pause; // Pause playback. - (void) play; // Resume playback. - (void) playTrack:(NSString *)x inContext:(NSString *)inContext; // Start playback of a track in the given context. @end // A Spotify track. @interface SpotifyTrack : SBObject @property (copy, readonly) NSString *artist; // The artist of the track. @property (copy, readonly) NSString *album; // The album of the track. @property (readonly) NSInteger discNumber; // The disc number of the track. @property (readonly) NSInteger duration; // The length of the track in seconds. @property (readonly) NSInteger playedCount; // The number of times this track has been played. @property (readonly) NSInteger trackNumber; // The index of the track in its album. @property (readonly) BOOL starred; // Is the track starred? @property (readonly) NSInteger popularity; // How popular is this track? 0-100 - (NSString *) id; // The ID of the item. @property (copy, readonly) NSString *name; // The name of the track. @property (copy, readonly) NSImage *artwork; // The track's album cover. @property (copy, readonly) NSString *albumArtist; // That album artist of the track. @property (copy) NSString *spotifyUrl; // The URL of the track. @end /* * Standard Suite */ // The application's top level scripting object. @interface SpotifyApplication (StandardSuite) @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. @end ================================================ FILE: BGMApp/BGMApp/Music Players/Swinsian.h ================================================ /* * Swinsian.h * * Generated with * sdef /Applications/Swinsian.app | sdp -fh --basename Swinsian */ #import #import @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 @property (copy) NSDictionary *properties; // All of the object's properties. @end // A color. @interface SwinsianColor : SBObject @end // A window. @interface SwinsianWindow : SBObject @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 *selection; // Currently seleted tracks @end /* * Swinsian Suite */ // The application @interface SwinsianApplication : SBApplication - (SBElementArray *) windows; - (SBElementArray *) playlists; - (SBElementArray *) smartPlaylists; - (SBElementArray *) normalPlaylists; - (SBElementArray *) libraries; - (SBElementArray *) tracks; - (SBElementArray *) 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 *) 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 *)tracks to:(SwinsianNormalPlaylist *)to; // add a track to a playlist - (void) notify; // show currently playing track notification - (void) rescanTags:(NSArray *)x; // rescan tags on tracks - (NSArray *) findTrack:(NSString *)x; // Finds tracks for the given path - (void) removeTracks:(NSArray *)tracks from:(SwinsianNormalPlaylist *)from; // remove tracks from a playlist @end // generic playlist type, subcasses include smart playlist and normal playlist @interface SwinsianPlaylist : SwinsianItem - (SBElementArray *) tracks; @property (copy) NSString *name; // the name of the playlist @property (readonly) BOOL smart; // is this a smart playlist @end @interface SwinsianLibrary : SwinsianItem - (SBElementArray *) 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 *) tracks; @end // a smart playlist @interface SwinsianSmartPlaylist : SwinsianPlaylist @end // a normal, non-smart, playlist @interface SwinsianNormalPlaylist : SwinsianPlaylist - (SBElementArray *) tracks; - (NSString *) id; // uuid @end // folder of playlists @interface SwinsianPlaylistFolder : SwinsianPlaylist - (SBElementArray *) playlists; - (NSString *) id; // uuid @end // an audio output device @interface SwinsianAudioDevice : SBObject @property (copy, readonly) NSString *name; // device name - (NSString *) id; // uuid - (void) setId: (NSString *) id; @end ================================================ FILE: BGMApp/BGMApp/Music Players/VLC.h ================================================ /* * VLC.h * * Generated with * sdef /Applications/VLC.app | sdp -fh --basename VLC */ #import #import @class VLCItem, VLCApplication, VLCColor, VLCDocument, VLCWindow, VLCAttributeRun, VLCCharacter, VLCParagraph, VLCText, VLCAttachment, VLCWord, VLCPrintSettings; enum VLCSavo { VLCSavoAsk = 'ask ' /* Ask the user whether or not to save the file. */, VLCSavoNo = 'no ' /* Do not save the file. */, VLCSavoYes = 'yes ' /* Save the file. */ }; typedef enum VLCSavo VLCSavo; enum VLCEnum { VLCEnumStandard = 'lwst' /* Standard PostScript error handling */, VLCEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */ }; typedef enum VLCEnum VLCEnum; @protocol VLCGenericMethods - (void) closeSaving:(VLCSavo)saving savingIn:(NSURL *)savingIn; // Close an object. - (void) delete; // Delete an object. - (void) duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location. - (BOOL) exists; // Verify if an object exists. - (void) moveTo:(SBObject *)to; // Move object(s) to a new location. - (void) saveAs:(NSString *)as in:(NSURL *)in_; // Save an object. - (void) fullscreen; // Toggle between fullscreen and windowed mode. - (void) GetURL; // Get a URL - (void) mute; // Mute the audio - (void) next; // Go to the next item in the playlist or the next chapter in the DVD/VCD. - (void) OpenURL; // Open a URL - (void) play; // Start playing the current playlistitem or pause it when it is already playing. - (void) previous; // Go to the previous item in the playlist or the previous chapter in the DVD/VCD. - (void) stepBackward; // Step the current playlist item backward the specified step width (default is 2) (1=extraShort, 2=short, 3=medium, 4=long). - (void) stepForward; // Step the current playlist item forward the specified step width (default is 2) (1=extraShort, 2=short, 3=medium, 4=long). - (void) stop; // Stop playing the current playlistitem - (void) volumeDown; // Bring the volume down by one step. There are 32 steps from 0 to 400% volume. - (void) volumeUp; // Bring the volume up by one step. There are 32 steps from 0 to 400% volume. @end /* * Standard Suite */ // A scriptable object. @interface VLCItem : SBObject @property (copy) NSDictionary *properties; // All of the object's properties. @end // An application's top level scripting object. @interface VLCApplication : SBApplication - (SBElementArray *) documents; - (SBElementArray *) windows; @property (readonly) BOOL frontmost; // Is this the frontmost (active) application? @property (copy, readonly) NSString *name; // The name of the application. @property (copy, readonly) NSString *version; // The version of the application. - (VLCDocument *) open:(NSURL *)x; // Open an object. - (void) print:(NSURL *)x printDialog:(BOOL)printDialog withProperties:(VLCPrintSettings *)withProperties; // Print an object. - (void) quitSaving:(VLCSavo)saving; // Quit an application. @end // A color. @interface VLCColor : VLCItem @end // A document. @interface VLCDocument : VLCItem @property (readonly) BOOL modified; // Has the document been modified since the last save? @property (copy) NSString *name; // The document's name. @property (copy) NSString *path; // The document's path. @end // A window. @interface VLCWindow : VLCItem @property NSRect bounds; // The bounding rectangle of the window. @property (readonly) BOOL closeable; // Whether the window has a close box. @property (copy, readonly) VLCDocument *document; // The document whose contents are being displayed in the window. @property (readonly) BOOL floating; // Whether the window floats. - (NSInteger) id; // The unique identifier of the window. @property NSInteger index; // The index of the window, ordered front to back. @property (readonly) BOOL miniaturizable; // Whether the window can be miniaturized. @property BOOL miniaturized; // Whether the window is currently miniaturized. @property (readonly) BOOL modal; // Whether the window is the application's current modal window. @property (copy) NSString *name; // The full title of the window. @property (readonly) BOOL resizable; // Whether the window can be resized. @property (readonly) BOOL titled; // Whether the window has a title bar. @property BOOL visible; // Whether the window is currently visible. @property (readonly) BOOL zoomable; // Whether the window can be zoomed. @property BOOL zoomed; // Whether the window is currently zoomed. @end /* * Text Suite */ // This subdivides the text into chunks that all have the same attributes. @interface VLCAttributeRun : VLCItem - (SBElementArray *) attachments; - (SBElementArray *) attributeRuns; - (SBElementArray *) characters; - (SBElementArray *) paragraphs; - (SBElementArray *) words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // This subdivides the text into characters. @interface VLCCharacter : VLCItem - (SBElementArray *) attachments; - (SBElementArray *) attributeRuns; - (SBElementArray *) characters; - (SBElementArray *) paragraphs; - (SBElementArray *) words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // This subdivides the text into paragraphs. @interface VLCParagraph : VLCItem - (SBElementArray *) attachments; - (SBElementArray *) attributeRuns; - (SBElementArray *) characters; - (SBElementArray *) paragraphs; - (SBElementArray *) words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // Rich (styled) text @interface VLCText : VLCItem - (SBElementArray *) attachments; - (SBElementArray *) attributeRuns; - (SBElementArray *) characters; - (SBElementArray *) paragraphs; - (SBElementArray *) words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // Represents an inline text attachment. This class is used mainly for make commands. @interface VLCAttachment : VLCText @property (copy) NSString *fileName; // The path to the file for the attachment @end // This subdivides the text into words. @interface VLCWord : VLCItem - (SBElementArray *) attachments; - (SBElementArray *) attributeRuns; - (SBElementArray *) characters; - (SBElementArray *) paragraphs; - (SBElementArray *) words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end /* * VLC suite */ // VLC's top level scripting object @interface VLCApplication (VLCSuite) @property NSInteger audioVolume; // The volume of the current playlist item from 0 to 4, where 4 is 400% @property NSInteger currentTime; // The current time of the current playlist item in seconds. @property (readonly) NSInteger durationOfCurrentItem; // The duration of the current playlist item in seconds. @property BOOL fullscreenMode; // indicates whether fullscreen is enabled or not @property (readonly) BOOL muted; // Is VLC currently muted? @property (copy, readonly) NSString *nameOfCurrentItem; // Name of the current playlist item. @property (copy, readonly) NSString *pathOfCurrentItem; // Path to the current playlist item. @property (readonly) BOOL playing; // Is VLC playing an item? @end /* * Type Definitions */ @interface VLCPrintSettings : SBObject @property NSInteger copies; // the number of copies of a document to be printed @property BOOL collating; // Should printed copies be collated? @property NSInteger startingPage; // the first page of the document to be printed @property NSInteger endingPage; // the last page of the document to be printed @property NSInteger pagesAcross; // number of logical pages laid across a physical page @property NSInteger pagesDown; // number of logical pages laid out down a physical page @property (copy) NSDate *requestedPrintTime; // the time at which the desktop printer should print the document @property VLCEnum errorHandling; // how errors are handled @property (copy) NSString *faxNumber; // for fax number @property (copy) NSString *targetPrinter; // for target printer @end ================================================ FILE: BGMApp/BGMApp/Music Players/VOX.h ================================================ /* * VOX.h * * Generated with * sdef /Applications/VOX.app | sdp -fh --basename VOX */ #import #import @class VoxApplication, VoxApplication; /* * Standard Suite */ // The application's top level scripting object. @interface VoxApplication : SBApplication @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. - (void) quit; // Quit an application. - (void) pause; // Pause playback. - (void) play; // Begin playback. - (void) playpause; // Toggle playback between playing and paused. - (void) next; // Skip to the next track in the playlist. - (void) previous; // Skip to the previous track in the playlist. - (void) shuffle; // Shuffle the tracks in the playlist. - (void) playUrl:(NSString *)x; // Play specified URL. - (void) addUrl:(NSString *)x; // Add specified URL to playlist - (void) rewindForward; // Rewind current track forward. - (void) rewindForwardFast; // Rewind current track forward fast. - (void) rewindBackward; // Rewind current track backward. - (void) rewindBackwardFast; // Rewind current track backward fast. - (void) increasVolume; // Increase volume. - (void) decreaseVolume; // Decrease volume. - (void) showHidePlaylist; // Show/Hide playlist. @end /* * Vox Suite */ // The application's top-level scripting object. @interface VoxApplication (VoxSuite) @property (copy, readonly) NSData *tiffArtworkData; // Current track artwork data in TIFF format. @property (copy, readonly) NSImage *artworkImage; // Current track artwork as an image. @property (readonly) NSInteger playerState; // Player state (playing = 1, paused = 0) @property (copy, readonly) NSString *track; // Current track title. @property (copy, readonly) NSString *trackUrl; // Current track URL. @property (copy, readonly) NSString *artist; // Current track artist. @property (copy, readonly) NSString *albumArtist; // Current track album artist. @property (copy, readonly) NSString *album; // Current track album. @property (copy, readonly) NSString *uniqueID; // Unique identifier for the current track. @property double currentTime; // The current playback position. @property (readonly) double totalTime; // The total time of the currently playing track. @property double playerVolume; // Player volume (0.0 to 1.0) @property NSInteger repeatState; // Player repeat state (none = 0, repeat one = 1, repeat all = 2) @end ================================================ FILE: BGMApp/BGMApp/Music Players/iTunes.h ================================================ /* * iTunes.h * * Generated with * sdef /Applications/iTunes.app | sdp -fh --basename iTunes */ #import #import @class iTunesPrintSettings, iTunesApplication, iTunesItem, iTunesAirPlayDevice, iTunesArtwork, iTunesEncoder, iTunesEQPreset, iTunesPlaylist, iTunesAudioCDPlaylist, iTunesLibraryPlaylist, iTunesRadioTunerPlaylist, iTunesSource, iTunesTrack, iTunesAudioCDTrack, iTunesFileTrack, iTunesSharedTrack, iTunesURLTrack, iTunesUserPlaylist, iTunesFolderPlaylist, iTunesVisual, iTunesWindow, iTunesBrowserWindow, iTunesEQWindow, iTunesPlaylistWindow; enum iTunesEKnd { iTunesEKndTrackListing = 'kTrk' /* a basic listing of tracks within a playlist */, iTunesEKndAlbumListing = 'kAlb' /* a listing of a playlist grouped by album */, iTunesEKndCdInsert = 'kCDi' /* a printout of the playlist for jewel case inserts */ }; typedef enum iTunesEKnd iTunesEKnd; enum iTunesEnum { iTunesEnumStandard = 'lwst' /* Standard PostScript error handling */, iTunesEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */ }; typedef enum iTunesEnum iTunesEnum; enum iTunesEPlS { iTunesEPlSStopped = 'kPSS', iTunesEPlSPlaying = 'kPSP', iTunesEPlSPaused = 'kPSp', iTunesEPlSFastForwarding = 'kPSF', iTunesEPlSRewinding = 'kPSR' }; typedef enum iTunesEPlS iTunesEPlS; enum iTunesERpt { iTunesERptOff = 'kRpO', iTunesERptOne = 'kRp1', iTunesERptAll = 'kAll' }; typedef enum iTunesERpt iTunesERpt; enum iTunesEVSz { iTunesEVSzSmall = 'kVSS', iTunesEVSzMedium = 'kVSM', iTunesEVSzLarge = 'kVSL' }; typedef enum iTunesEVSz iTunesEVSz; enum iTunesESrc { iTunesESrcLibrary = 'kLib', iTunesESrcIPod = 'kPod', iTunesESrcAudioCD = 'kACD', iTunesESrcMP3CD = 'kMCD', iTunesESrcRadioTuner = 'kTun', iTunesESrcSharedLibrary = 'kShd', iTunesESrcUnknown = 'kUnk' }; typedef enum iTunesESrc iTunesESrc; enum iTunesESrA { iTunesESrAAlbums = 'kSrL' /* albums only */, iTunesESrAAll = 'kAll' /* all text fields */, iTunesESrAArtists = 'kSrR' /* artists only */, iTunesESrAComposers = 'kSrC' /* composers only */, iTunesESrADisplayed = 'kSrV' /* visible text fields */, iTunesESrASongs = 'kSrS' /* song names only */ }; typedef enum iTunesESrA iTunesESrA; enum iTunesESpK { iTunesESpKNone = 'kNon', iTunesESpKBooks = 'kSpA', iTunesESpKFolder = 'kSpF', iTunesESpKGenius = 'kSpG', iTunesESpKITunesU = 'kSpU', iTunesESpKLibrary = 'kSpL', iTunesESpKMovies = 'kSpI', iTunesESpKMusic = 'kSpZ', iTunesESpKPodcasts = 'kSpP', iTunesESpKPurchasedMusic = 'kSpM', iTunesESpKTVShows = 'kSpT' }; typedef enum iTunesESpK iTunesESpK; enum iTunesEVdK { iTunesEVdKNone = 'kNon' /* not a video or unknown video kind */, iTunesEVdKHomeVideo = 'kVdH' /* home video track */, iTunesEVdKMovie = 'kVdM' /* movie track */, iTunesEVdKMusicVideo = 'kVdV' /* music video track */, iTunesEVdKTVShow = 'kVdT' /* TV show track */ }; typedef enum iTunesEVdK iTunesEVdK; enum iTunesERtK { iTunesERtKUser = 'kRtU' /* user-specified rating */, iTunesERtKComputed = 'kRtC' /* iTunes-computed rating */ }; typedef enum iTunesERtK iTunesERtK; enum iTunesEAPD { iTunesEAPDComputer = 'kAPC', iTunesEAPDAirPortExpress = 'kAPX', iTunesEAPDAppleTV = 'kAPT', iTunesEAPDAirPlayDevice = 'kAPO', iTunesEAPDUnknown = 'kAPU' }; typedef enum iTunesEAPD iTunesEAPD; /* * Standard Suite */ @interface iTunesPrintSettings : SBObject @property (readonly) NSInteger copies; // the number of copies of a document to be printed @property (readonly) BOOL collating; // Should printed copies be collated? @property (readonly) NSInteger startingPage; // the first page of the document to be printed @property (readonly) NSInteger endingPage; // the last page of the document to be printed @property (readonly) NSInteger pagesAcross; // number of logical pages laid across a physical page @property (readonly) NSInteger pagesDown; // number of logical pages laid out down a physical page @property (readonly) iTunesEnum errorHandling; // how errors are handled @property (copy, readonly) NSDate *requestedPrintTime; // the time at which the desktop printer should print the document @property (copy, readonly) NSArray *printerFeatures; // printer specific options @property (copy, readonly) NSString *faxNumber; // for fax number @property (copy, readonly) NSString *targetPrinter; // for target printer - (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme; // Print the specified object(s) - (void) close; // Close an object - (void) delete; // Delete an element from an object - (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s) - (BOOL) exists; // Verify if an object exists - (void) open; // open the specified object(s) - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. @end /* * iTunes Suite */ // The application program @interface iTunesApplication : SBApplication - (SBElementArray *) AirPlayDevices; - (SBElementArray *) browserWindows; - (SBElementArray *) encoders; - (SBElementArray *) EQPresets; - (SBElementArray *) EQWindows; - (SBElementArray *) playlistWindows; - (SBElementArray *) sources; - (SBElementArray *) visuals; - (SBElementArray *) windows; @property (readonly) BOOL AirPlayEnabled; // is AirPlay currently enabled? @property (readonly) BOOL converting; // is a track currently being converted? @property (copy) NSArray *currentAirPlayDevices; // the currently selected AirPlay device(s) @property (copy) iTunesEncoder *currentEncoder; // the currently selected encoder (MP3, AIFF, WAV, etc.) @property (copy) iTunesEQPreset *currentEQPreset; // the currently selected equalizer preset @property (copy, readonly) iTunesPlaylist *currentPlaylist; // the playlist containing the currently targeted track @property (copy, readonly) NSString *currentStreamTitle; // the name of the current song in the playing stream (provided by streaming server) @property (copy, readonly) NSString *currentStreamURL; // the URL of the playing stream or streaming web site (provided by streaming server) @property (copy, readonly) iTunesTrack *currentTrack; // the current targeted track @property (copy) iTunesVisual *currentVisual; // the currently selected visual plug-in @property BOOL EQEnabled; // is the equalizer enabled? @property BOOL fixedIndexing; // true if all AppleScript track indices should be independent of the play order of the owning playlist. @property BOOL frontmost; // is iTunes the frontmost application? @property BOOL fullScreen; // are visuals displayed using the entire screen? @property (copy, readonly) NSString *name; // the name of the application @property BOOL mute; // has the sound output been muted? @property double playerPosition; // the player’s position within the currently playing track in seconds. @property (readonly) iTunesEPlS playerState; // is iTunes stopped, paused, or playing? @property (copy, readonly) SBObject *selection; // the selection visible to the user @property NSInteger soundVolume; // the sound output volume (0 = minimum, 100 = maximum) @property (copy, readonly) NSString *version; // the version of iTunes @property BOOL visualsEnabled; // are visuals currently being displayed? @property iTunesEVSz visualSize; // the size of the displayed visual @property (copy, readonly) NSString *iAdIdentifier; // the iAd identifier - (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme; // Print the specified object(s) - (void) run; // run iTunes - (void) quit; // quit iTunes - (iTunesTrack *) add:(NSArray *)x to:(SBObject *)to; // add one or more files to a playlist - (void) backTrack; // reposition to beginning of current track or go to previous track if already at start of current track - (iTunesTrack *) convert:(NSArray *)x; // convert one or more files or tracks - (void) fastForward; // skip forward in a playing track - (void) nextTrack; // advance to the next track in the current playlist - (void) pause; // pause playback - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. - (void) playpause; // toggle the playing/paused state of the current track - (void) previousTrack; // return to the previous track in the current playlist - (void) resume; // disable fast forward/rewind and resume playback, if playing. - (void) rewind; // skip backwards in a playing track - (void) stop; // stop playback - (void) update; // update the specified iPod - (void) eject; // eject the specified iPod - (void) subscribe:(NSString *)x; // subscribe to a podcast feed - (void) updateAllPodcasts; // update all subscribed podcast feeds - (void) updatePodcast; // update podcast feed - (void) openLocation:(NSString *)x; // Opens a Music Store or audio stream URL @end // an item @interface iTunesItem : SBObject @property (copy, readonly) SBObject *container; // the container of the item - (NSInteger) id; // the id of the item @property (readonly) NSInteger index; // The index of the item in internal application order. @property (copy) NSString *name; // the name of the item @property (copy, readonly) NSString *persistentID; // the id of the item as a hexadecimal string. This id does not change over time. @property (copy) NSDictionary *properties; // every property of the item - (void) printPrintDialog:(BOOL)printDialog withProperties:(iTunesPrintSettings *)withProperties kind:(iTunesEKnd)kind theme:(NSString *)theme; // Print the specified object(s) - (void) close; // Close an object - (void) delete; // Delete an element from an object - (SBObject *) duplicateTo:(SBObject *)to; // Duplicate one or more object(s) - (BOOL) exists; // Verify if an object exists - (void) open; // open the specified object(s) - (void) playOnce:(BOOL)once; // play the current track or the specified track or file. - (void) reveal; // reveal and select a track or playlist @end // an AirPlay device @interface iTunesAirPlayDevice : iTunesItem @property (readonly) BOOL active; // is the device currently being played to? @property (readonly) BOOL available; // is the device currently available? @property (readonly) iTunesEAPD kind; // the kind of the device @property (copy, readonly) NSString *networkAddress; // the network (MAC) address of the device - (BOOL) protected; // is the device password- or passcode-protected? @property BOOL selected; // is the device currently selected? @property (readonly) BOOL supportsAudio; // does the device support audio playback? @property (readonly) BOOL supportsVideo; // does the device support video playback? @property NSInteger soundVolume; // the output volume for the device (0 = minimum, 100 = maximum) @end // a piece of art within a track @interface iTunesArtwork : iTunesItem @property (copy) NSImage *data; // data for this artwork, in the form of a picture @property (copy) NSString *objectDescription; // description of artwork as a string @property (readonly) BOOL downloaded; // was this artwork downloaded by iTunes? @property (copy, readonly) NSNumber *format; // the data format for this piece of artwork @property NSInteger kind; // kind or purpose of this piece of artwork @property (copy) NSData *rawData; // data for this artwork, in original format @end // converts a track to a specific file format @interface iTunesEncoder : iTunesItem @property (copy, readonly) NSString *format; // the data format created by the encoder @end // equalizer preset configuration @interface iTunesEQPreset : iTunesItem @property double band1; // the equalizer 32 Hz band level (-12.0 dB to +12.0 dB) @property double band2; // the equalizer 64 Hz band level (-12.0 dB to +12.0 dB) @property double band3; // the equalizer 125 Hz band level (-12.0 dB to +12.0 dB) @property double band4; // the equalizer 250 Hz band level (-12.0 dB to +12.0 dB) @property double band5; // the equalizer 500 Hz band level (-12.0 dB to +12.0 dB) @property double band6; // the equalizer 1 kHz band level (-12.0 dB to +12.0 dB) @property double band7; // the equalizer 2 kHz band level (-12.0 dB to +12.0 dB) @property double band8; // the equalizer 4 kHz band level (-12.0 dB to +12.0 dB) @property double band9; // the equalizer 8 kHz band level (-12.0 dB to +12.0 dB) @property double band10; // the equalizer 16 kHz band level (-12.0 dB to +12.0 dB) @property (readonly) BOOL modifiable; // can this preset be modified? @property double preamp; // the equalizer preamp level (-12.0 dB to +12.0 dB) @property BOOL updateTracks; // should tracks which refer to this preset be updated when the preset is renamed or deleted? @end // a list of songs/streams @interface iTunesPlaylist : iTunesItem - (SBElementArray *) tracks; @property (readonly) NSInteger duration; // the total length of all songs (in seconds) @property (copy) NSString *name; // the name of the playlist @property BOOL loved; // is this plalist loved? @property (copy, readonly) iTunesPlaylist *parent; // folder which contains this playlist (if any) @property BOOL shuffle; // play the songs in this playlist in random order? @property (readonly) NSInteger size; // the total size of all songs (in bytes) @property iTunesERpt songRepeat; // playback repeat mode @property (readonly) iTunesESpK specialKind; // special playlist kind @property (copy, readonly) NSString *time; // the length of all songs in MM:SS format @property (readonly) BOOL visible; // is this playlist visible in the Source list? - (void) moveTo:(SBObject *)to; // Move playlist(s) to a new location - (iTunesTrack *) searchFor:(NSString *)for_ only:(iTunesESrA)only; // search a playlist for tracks matching the search string. Identical to entering search text in the Search field in iTunes. @end // a playlist representing an audio CD @interface iTunesAudioCDPlaylist : iTunesPlaylist - (SBElementArray *) audioCDTracks; @property (copy) NSString *artist; // the artist of the CD @property BOOL compilation; // is this CD a compilation album? @property (copy) NSString *composer; // the composer of the CD @property NSInteger discCount; // the total number of discs in this CD’s album @property NSInteger discNumber; // the index of this CD disc in the source album @property (copy) NSString *genre; // the genre of the CD @property NSInteger year; // the year the album was recorded/released @end // the master music library playlist @interface iTunesLibraryPlaylist : iTunesPlaylist - (SBElementArray *) fileTracks; - (SBElementArray *) URLTracks; - (SBElementArray *) sharedTracks; @end // the radio tuner playlist @interface iTunesRadioTunerPlaylist : iTunesPlaylist - (SBElementArray *) URLTracks; @end // a music source (music library, CD, device, etc.) @interface iTunesSource : iTunesItem - (SBElementArray *) audioCDPlaylists; - (SBElementArray *) libraryPlaylists; - (SBElementArray *) playlists; - (SBElementArray *) radioTunerPlaylists; - (SBElementArray *) userPlaylists; @property (readonly) long long capacity; // the total size of the source if it has a fixed size @property (readonly) long long freeSpace; // the free space on the source if it has a fixed size @property (readonly) iTunesESrc kind; - (void) update; // update the specified iPod - (void) eject; // eject the specified iPod @end // playable audio source @interface iTunesTrack : iTunesItem - (SBElementArray *) artworks; @property (copy) NSString *album; // the album name of the track @property (copy) NSString *albumArtist; // the album artist of the track @property BOOL albumLoved; // is the album for this track loved? @property NSInteger albumRating; // the rating of the album for this track (0 to 100) @property (readonly) iTunesERtK albumRatingKind; // the rating kind of the album rating for this track @property (copy) NSString *artist; // the artist/source of the track @property (readonly) NSInteger bitRate; // the bit rate of the track (in kbps) @property double bookmark; // the bookmark time of the track in seconds @property BOOL bookmarkable; // is the playback position for this track remembered? @property NSInteger bpm; // the tempo of this track in beats per minute @property (copy) NSString *category; // the category of the track @property (copy) NSString *comment; // freeform notes about the track @property BOOL compilation; // is this track from a compilation album? @property (copy) NSString *composer; // the composer of the track @property (readonly) NSInteger databaseID; // the common, unique ID for this track. If two tracks in different playlists have the same database ID, they are sharing the same data. @property (copy, readonly) NSDate *dateAdded; // the date the track was added to the playlist @property (copy) NSString *objectDescription; // the description of the track @property NSInteger discCount; // the total number of discs in the source album @property NSInteger discNumber; // the index of the disc containing this track on the source album @property (readonly) double duration; // the length of the track in seconds @property BOOL enabled; // is this track checked for playback? @property (copy) NSString *episodeID; // the episode ID of the track @property NSInteger episodeNumber; // the episode number of the track @property (copy) NSString *EQ; // the name of the EQ preset of the track @property double finish; // the stop time of the track in seconds @property BOOL gapless; // is this track from a gapless album? @property (copy) NSString *genre; // the music/audio genre (category) of the track @property (copy) NSString *grouping; // the grouping (piece) of the track. Generally used to denote movements within a classical work. @property (readonly) BOOL iTunesU; // is this track an iTunes U episode? @property (copy, readonly) NSString *kind; // a text description of the track @property (copy) NSString *longDescription; @property BOOL loved; // is this track loved? @property (copy) NSString *lyrics; // the lyrics of the track @property (copy, readonly) NSDate *modificationDate; // the modification date of the content of this track @property NSInteger playedCount; // number of times this track has been played @property (copy) NSDate *playedDate; // the date and time this track was last played @property (readonly) BOOL podcast; // is this track a podcast episode? @property NSInteger rating; // the rating of this track (0 to 100) @property (readonly) iTunesERtK ratingKind; // the rating kind of this track @property (copy, readonly) NSDate *releaseDate; // the release date of this track @property (readonly) NSInteger sampleRate; // the sample rate of the track (in Hz) @property NSInteger seasonNumber; // the season number of the track @property BOOL shufflable; // is this track included when shuffling? @property NSInteger skippedCount; // number of times this track has been skipped @property (copy) NSDate *skippedDate; // the date and time this track was last skipped @property (copy) NSString *show; // the show name of the track @property (copy) NSString *sortAlbum; // override string to use for the track when sorting by album @property (copy) NSString *sortArtist; // override string to use for the track when sorting by artist @property (copy) NSString *sortAlbumArtist; // override string to use for the track when sorting by album artist @property (copy) NSString *sortName; // override string to use for the track when sorting by name @property (copy) NSString *sortComposer; // override string to use for the track when sorting by composer @property (copy) NSString *sortShow; // override string to use for the track when sorting by show name @property (readonly) long long size; // the size of the track (in bytes) @property double start; // the start time of the track in seconds @property (copy, readonly) NSString *time; // the length of the track in MM:SS format @property NSInteger trackCount; // the total number of tracks on the source album @property NSInteger trackNumber; // the index of the track on the source album @property BOOL unplayed; // is this track unplayed? @property iTunesEVdK videoKind; // kind of video track @property NSInteger volumeAdjustment; // relative volume adjustment of the track (-100% to 100%) @property NSInteger year; // the year the track was recorded/released @end // a track on an audio CD @interface iTunesAudioCDTrack : iTunesTrack @property (copy, readonly) NSURL *location; // the location of the file represented by this track @end // a track representing an audio file (MP3, AIFF, etc.) @interface iTunesFileTrack : iTunesTrack @property (copy) NSURL *location; // the location of the file represented by this track - (void) refresh; // update file track information from the current information in the track’s file @end // a track residing in a shared library @interface iTunesSharedTrack : iTunesTrack @end // a track representing a network stream @interface iTunesURLTrack : iTunesTrack @property (copy) NSString *address; // the URL for this track - (void) download; // download podcast episode @end // custom playlists created by the user @interface iTunesUserPlaylist : iTunesPlaylist - (SBElementArray *) fileTracks; - (SBElementArray *) URLTracks; - (SBElementArray *) sharedTracks; @property BOOL shared; // is this playlist shared? @property (readonly) BOOL smart; // is this a Smart Playlist? @end // a folder that contains other playlists @interface iTunesFolderPlaylist : iTunesUserPlaylist @end // a visual plug-in @interface iTunesVisual : iTunesItem @end // any window @interface iTunesWindow : iTunesItem @property NSRect bounds; // the boundary rectangle for the window @property (readonly) BOOL closeable; // does the window have a close box? @property (readonly) BOOL collapseable; // does the window have a collapse (windowshade) box? @property BOOL collapsed; // is the window collapsed? @property NSPoint position; // the upper left position of the window @property (readonly) BOOL resizable; // is the window resizable? @property BOOL visible; // is the window visible? @property (readonly) BOOL zoomable; // is the window zoomable? @property BOOL zoomed; // is the window zoomed? @end // the main iTunes window @interface iTunesBrowserWindow : iTunesWindow @property BOOL minimized; // is the small player visible? @property (copy, readonly) SBObject *selection; // the selected songs @property (copy) iTunesPlaylist *view; // the playlist currently displayed in the window @end // the iTunes equalizer window @interface iTunesEQWindow : iTunesWindow @property BOOL minimized; // is the small EQ window visible? @end // a sub-window showing a single playlist @interface iTunesPlaylistWindow : iTunesWindow @property (copy, readonly) SBObject *selection; // the selected songs @property (copy, readonly) iTunesPlaylist *view; // the playlist displayed in the window @end ================================================ FILE: BGMApp/BGMApp/Preferences/BGMAboutPanel.h ================================================ // 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 . // // BGMAboutPanel.h // BGMApp // // Copyright © 2016 Kyle Neideck // // This class manages the "About Background Music" window. // // System Includes #import NS_ASSUME_NONNULL_BEGIN @interface BGMAboutPanel : NSObject - (instancetype)initWithPanel:(NSPanel*)inAboutPanel licenseView:(NSTextView*)inLicenseView; - (void) show; @end @interface BGMLinkField : NSTextField @end NS_ASSUME_NONNULL_END ================================================ FILE: BGMApp/BGMApp/Preferences/BGMAboutPanel.m ================================================ // 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 . // // BGMAboutPanel.m // BGMApp // // Copyright © 2016, 2024 Kyle Neideck // // Self Include #import "BGMAboutPanel.h" // Local Includes #import "BGM_Types.h" // PublicUtility Includes #include "CADebugMacros.h" NS_ASSUME_NONNULL_BEGIN static NSInteger const kVersionLabelTag = 1; static NSInteger const kCopyrightLabelTag = 2; static NSInteger const kProjectWebsiteLabelTag = 3; static NSInteger const kContributorsLabelTag = 4; @implementation BGMAboutPanel { NSPanel* aboutPanel; NSTextField* versionLabel; NSTextField* copyrightLabel; NSTextField* websiteLabel; NSTextField* contributorsLabel; NSTextView* licenseView; } - (instancetype)initWithPanel:(NSPanel*)inAboutPanel licenseView:(NSTextView*)inLicenseView { if ((self = [super init])) { aboutPanel = inAboutPanel; versionLabel = [[aboutPanel contentView] viewWithTag:kVersionLabelTag]; copyrightLabel = [[aboutPanel contentView] viewWithTag:kCopyrightLabelTag]; websiteLabel = [[aboutPanel contentView] viewWithTag:kProjectWebsiteLabelTag]; contributorsLabel = [[aboutPanel contentView] viewWithTag:kContributorsLabelTag]; licenseView = inLicenseView; [self initAboutPanel]; } return self; } - (void) initAboutPanel { // Set up the About Background Music window NSBundle* bundle = [NSBundle mainBundle]; if (bundle == nil) { NSLog(@"Background Music: BGMAboutPanel::initAboutPanel: Could not find main bundle"); } else { // Version number label NSString* __nullable version = [[bundle infoDictionary] objectForKey:@"CFBundleShortVersionString"]; if (version) { versionLabel.stringValue = [NSString stringWithFormat:@"Version %@", version]; } // Copyright notice label NSString* __nullable copyrightNotice = [[bundle infoDictionary] objectForKey:@"NSHumanReadableCopyright"]; if (copyrightNotice) { // Remove the part that we replace with a link. copyrightLabel.stringValue = [((NSString*)copyrightNotice) stringByReplacingOccurrencesOfString:contributorsLabel.stringValue withString:@""]; } // Project website link label websiteLabel.selectable = YES; 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: linkFont }]; // Contributors link label // TODO: Proper credits (i.e. in the app instead of just a link) contributorsLabel.selectable = YES; contributorsLabel.allowsEditingTextAttributes = YES; NSString* contributorsURL = [NSString stringWithUTF8String:kBGMContributorsURL]; NSFont* cLinkFont = contributorsLabel.font ? contributorsLabel.font : [NSFont labelFontOfSize:0.0]; contributorsLabel.attributedStringValue = [[NSAttributedString alloc] initWithString:contributorsLabel.stringValue attributes:@{ NSLinkAttributeName: contributorsURL, NSFontAttributeName: cLinkFont }]; // Load the text of the license into the text view NSString* __nullable licensePath = [bundle pathForResource:@"LICENSE" ofType:nil]; NSError* err; NSString* __nullable licenseStr = (!licensePath ? nil : [NSString stringWithContentsOfFile:(NSString*)licensePath encoding:NSASCIIStringEncoding error:&err]); if (err || !licenseStr || [licenseStr isEqualToString:@""]) { NSLog(@"Error loading license file: %@", err); licenseStr = @"Error: could not open license file."; } licenseView.string = (NSString*)licenseStr; NSFont* __nullable font = [NSFont fontWithName:@"Andale Mono" size:0.0]; if (font) { licenseView.textStorage.font = font; } } } - (void) show { DebugMsg("BGMAboutPanel::showAboutPanel: Opening \"About Background Music\" panel"); // We have to make aboutPanel visible before calling [NSApp activateIgnoringOtherApps:YES] // or the app won't be activated the first time (not sure why it only happens the first // time) and aboutPanel won't open. WindowServer logs this explanation: // 0[SetFrontProcessWithInfo]: CPS: Rejecting the request for pid 1234 due to the activation count being 0; launch ts=19302059379458, current time=19314267188375, window count = 0. [aboutPanel setIsVisible:YES]; [aboutPanel makeKeyAndOrderFront:self]; // On macOS 14.4, aboutPanel needs "Release When Closed" unchecked in MainMenu.xib. Otherwise, // aboutPanel will never open again if you click the close button. // This is deprecated for NSApplication.activate, but that stops aboutPanel from ever being shown. [NSApp activateIgnoringOtherApps:YES]; DebugMsg("BGMAboutPanel::showAboutPanel: Finished opening panel. " "aboutPanel.isVisible %d, aboutPanel.isKeyWindow %d, NSApp.isActive %d", aboutPanel.isVisible, aboutPanel.isKeyWindow, NSApp.isActive); } @end @implementation BGMLinkField - (void) resetCursorRects { // Change the mouse cursor when hovering over the link. (It does change by default, but only after // you've clicked it once.) [self addCursorRect:self.bounds cursor:[NSCursor pointingHandCursor]]; } @end NS_ASSUME_NONNULL_END ================================================ FILE: BGMApp/BGMApp/Preferences/BGMAutoPauseMusicPrefs.h ================================================ // 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 . // // BGMAutoPauseMusicPrefs.h // BGMApp // // Copyright © 2016 Kyle Neideck // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMMusicPlayers.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMAutoPauseMusicPrefs : NSObject - (id) initWithPreferencesMenu:(NSMenu*)inPrefsMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Preferences/BGMAutoPauseMusicPrefs.mm ================================================ // 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 . // // BGMAutoPauseMusicPrefs.mm // BGMApp // // Copyright © 2016, 2019 Kyle Neideck // // Self Includes #import "BGMAutoPauseMusicPrefs.h" // Local Includes #import "BGM_Types.h" #import "BGMMusicPlayer.h" #pragma clang assume_nonnull begin static float const kMenuItemIconScalingFactor = 1.15f; static NSInteger const kPrefsMenuAutoPauseHeaderTag = 1; @implementation BGMAutoPauseMusicPrefs { BGMAudioDeviceManager* audioDevices; BGMMusicPlayers* musicPlayers; NSMenu* prefsMenu; NSArray* musicPlayerMenuItems; } - (id) initWithPreferencesMenu:(NSMenu*)inPrefsMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers { if ((self = [super init])) { prefsMenu = inPrefsMenu; audioDevices = inAudioDevices; musicPlayers = inMusicPlayers; musicPlayerMenuItems = @[]; [self initPreferencesMenuSection]; } return self; } - (void) initPreferencesMenuSection { // Add the menu items related to auto-pausing music to the Preferences submenu // The index to start inserting music player menu items at NSInteger musicPlayerItemsIndex = [prefsMenu indexOfItemWithTag:kPrefsMenuAutoPauseHeaderTag] + 1; // Insert the menu items used to change the music player app. for (id musicPlayer in musicPlayers.musicPlayers) { // Create an menu item for this music player. NSMenuItem* menuItem = [prefsMenu insertItemWithTitle:musicPlayer.name action:@selector(handleMusicPlayerChange:) keyEquivalent:@"" atIndex:musicPlayerItemsIndex]; menuItem.toolTip = musicPlayer.toolTip; musicPlayerMenuItems = [musicPlayerMenuItems arrayByAddingObject:menuItem]; // Associate the music player with the menu item menuItem.representedObject = musicPlayer; // Show the menu item for the selected music player as selected if (musicPlayers.selectedMusicPlayer == musicPlayer) { menuItem.state = NSOnState; } // Set the menu item's icon NSImage* __nullable icon = musicPlayer.icon; if (icon == nil) { // Set a blank icon so the text lines up icon = [NSImage new]; } // Size the icon relative to the size of the item's text CGFloat length = [NSFont menuBarFontOfSize:0].pointSize * kMenuItemIconScalingFactor; icon.size = NSMakeSize(length, length); menuItem.image = icon; menuItem.target = self; menuItem.indentationLevel = 1; } } - (void) handleMusicPlayerChange:(NSMenuItem*)sender { // Set the new music player as the selected music player id musicPlayer = sender.representedObject; NSAssert(musicPlayer, @"BGMAutoPauseMusicPrefs::handleMusicPlayerChange: !musicPlayer"); musicPlayers.selectedMusicPlayer = musicPlayer; // Select/deselect the menu items for (NSMenuItem* item in musicPlayerMenuItems) { BOOL isNewlySelectedMusicPlayer = (item == sender); item.state = (isNewlySelectedMusicPlayer ? NSOnState : NSOffState); } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Preferences/BGMPreferencesMenu.h ================================================ // 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 . // // BGMPreferencesMenu.h // BGMApp // // Copyright © 2016, 2018, 2019 Kyle Neideck // // Handles the preferences menu UI. The user's preference changes are often passed directly to the driver rather // than to other BGMApp classes. // // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMMusicPlayers.h" #import "BGMStatusBarItem.h" #import "BGMUserDefaults.h" // System Includes #import NS_ASSUME_NONNULL_BEGIN @interface BGMPreferencesMenu : NSObject - (id) initWithBGMMenu:(NSMenu*)inBGMMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers statusBarItem:(BGMStatusBarItem*)inStatusBarItem aboutPanel:(NSPanel*)inAboutPanel aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView userDefaults:(BGMUserDefaults*)inUserDefaults; @end NS_ASSUME_NONNULL_END ================================================ FILE: BGMApp/BGMApp/Preferences/BGMPreferencesMenu.mm ================================================ // 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 . // // BGMPreferencesMenu.mm // BGMApp // // Copyright © 2016, 2018, 2019 Kyle Neideck // // Self Include #import "BGMPreferencesMenu.h" // Local Includes #import "BGMAutoPauseMusicPrefs.h" #import "BGMAboutPanel.h" // System Includes #import NS_ASSUME_NONNULL_BEGIN // Interface Builder tags static NSInteger const kPreferencesMenuItemTag = 1; static NSInteger const kBGMIconMenuItemTag = 2; static NSInteger const kVolumeIconMenuItemTag = 3; static NSInteger const kAboutPanelMenuItemTag = 4; @implementation BGMPreferencesMenu { // Menu sections/items BGMAutoPauseMusicPrefs* autoPauseMusicPrefs; NSMenuItem* bgmIconMenuItem; NSMenuItem* volumeIconMenuItem; // The menu item you press to open BGMApp's main menu. BGMStatusBarItem* statusBarItem; // The About Background Music window BGMAboutPanel* aboutPanel; // Delay preferences NSMenuItem* pauseDelayMenuItem; NSMenuItem* maxUnpauseDelayMenuItem; NSSlider* pauseDelaySlider; NSSlider* maxUnpauseDelaySlider; NSTextField* pauseDelayLabel; NSTextField* maxUnpauseDelayLabel; BGMUserDefaults* userDefaults; } - (id) initWithBGMMenu:(NSMenu*)inBGMMenu audioDevices:(BGMAudioDeviceManager*)inAudioDevices musicPlayers:(BGMMusicPlayers*)inMusicPlayers statusBarItem:(BGMStatusBarItem*)inStatusBarItem aboutPanel:(NSPanel*)inAboutPanel aboutPanelLicenseView:(NSTextView*)inAboutPanelLicenseView userDefaults:(BGMUserDefaults*)inUserDefaults { if ((self = [super init])) { NSMenu* prefsMenu = [[inBGMMenu itemWithTag:kPreferencesMenuItemTag] submenu]; autoPauseMusicPrefs = [[BGMAutoPauseMusicPrefs alloc] initWithPreferencesMenu:prefsMenu audioDevices:inAudioDevices musicPlayers:inMusicPlayers]; aboutPanel = [[BGMAboutPanel alloc] initWithPanel:inAboutPanel licenseView:inAboutPanelLicenseView]; statusBarItem = inStatusBarItem; // Set up the menu items under the "Status Bar Icon" heading. bgmIconMenuItem = [prefsMenu itemWithTag:kBGMIconMenuItemTag]; bgmIconMenuItem.state = (statusBarItem.icon == BGMFermataStatusBarIcon) ? NSOnState : NSOffState; [bgmIconMenuItem setTarget:self]; [bgmIconMenuItem setAction:@selector(useBGMStatusBarIcon)]; volumeIconMenuItem = [prefsMenu itemWithTag:kVolumeIconMenuItemTag]; volumeIconMenuItem.state = (statusBarItem.icon == BGMVolumeStatusBarIcon) ? NSOnState : NSOffState; [volumeIconMenuItem setTarget:self]; [volumeIconMenuItem setAction:@selector(useVolumeStatusBarIcon)]; // Set up the "About Background Music" menu item NSMenuItem* aboutMenuItem = [prefsMenu itemWithTag:kAboutPanelMenuItemTag]; [aboutMenuItem setTarget:aboutPanel]; [aboutMenuItem setAction:@selector(show)]; // Set up delay preferences userDefaults = inUserDefaults; [self setupDelayPreferences:prefsMenu]; } return self; } - (void) useBGMStatusBarIcon { // Change the icon. statusBarItem.icon = BGMFermataStatusBarIcon; // Select/deselect the menu items. bgmIconMenuItem.state = NSOnState; volumeIconMenuItem.state = NSOffState; } - (void) useVolumeStatusBarIcon { // TODO: Maybe we should show a message that tells the user how to hide the built-in volume // icon. They probably won't want two status bar items that look the same. Or we might be // able to automatically hide the built-in icon while BGMApp is running and show it again // when BGMApp is closed. // Change the icon. statusBarItem.icon = BGMVolumeStatusBarIcon; // Select/deselect the menu items. bgmIconMenuItem.state = NSOffState; volumeIconMenuItem.state = NSOnState; } - (void) setupDelayPreferences:(NSMenu*)prefsMenu { // Create delay preference menu items dynamically // Add separator [prefsMenu addItem:[NSMenuItem separatorItem]]; // Add "Auto-pause Delays" header NSMenuItem* delaysHeader = [[NSMenuItem alloc] initWithTitle:@"Auto-pause Delays" action:nil keyEquivalent:@""]; delaysHeader.enabled = NO; [prefsMenu addItem:delaysHeader]; // Create pause delay menu item with slider pauseDelayMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; NSView* pauseDelayView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 280, 25)]; // Pause delay label NSTextField* pauseDelayTitleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 5, 100, 15)]; pauseDelayTitleLabel.stringValue = @"Pause Delay:"; pauseDelayTitleLabel.editable = NO; pauseDelayTitleLabel.bordered = NO; pauseDelayTitleLabel.backgroundColor = [NSColor clearColor]; pauseDelayTitleLabel.font = [NSFont menuFontOfSize:13]; [pauseDelayView addSubview:pauseDelayTitleLabel]; // Pause delay slider pauseDelaySlider = [[NSSlider alloc] initWithFrame:NSMakeRect(115, 5, 100, 15)]; [pauseDelayView addSubview:pauseDelaySlider]; // Pause delay value label pauseDelayLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(220, 5, 55, 15)]; pauseDelayLabel.editable = NO; pauseDelayLabel.bordered = NO; pauseDelayLabel.backgroundColor = [NSColor clearColor]; pauseDelayLabel.font = [NSFont menuFontOfSize:11]; [pauseDelayView addSubview:pauseDelayLabel]; pauseDelayMenuItem.view = pauseDelayView; [prefsMenu addItem:pauseDelayMenuItem]; // Create max unpause delay menu item with slider maxUnpauseDelayMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; NSView* maxUnpauseDelayView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 280, 25)]; // Max unpause delay label NSTextField* maxUnpauseDelayTitleLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 5, 100, 15)]; maxUnpauseDelayTitleLabel.stringValue = @"Max Unpause:"; maxUnpauseDelayTitleLabel.editable = NO; maxUnpauseDelayTitleLabel.bordered = NO; maxUnpauseDelayTitleLabel.backgroundColor = [NSColor clearColor]; maxUnpauseDelayTitleLabel.font = [NSFont menuFontOfSize:13]; [maxUnpauseDelayView addSubview:maxUnpauseDelayTitleLabel]; // Max unpause delay slider maxUnpauseDelaySlider = [[NSSlider alloc] initWithFrame:NSMakeRect(115, 5, 100, 15)]; [maxUnpauseDelayView addSubview:maxUnpauseDelaySlider]; // Max unpause delay value label maxUnpauseDelayLabel = [[NSTextField alloc] initWithFrame:NSMakeRect(220, 5, 55, 15)]; maxUnpauseDelayLabel.editable = NO; maxUnpauseDelayLabel.bordered = NO; maxUnpauseDelayLabel.backgroundColor = [NSColor clearColor]; maxUnpauseDelayLabel.font = [NSFont menuFontOfSize:11]; [maxUnpauseDelayView addSubview:maxUnpauseDelayLabel]; maxUnpauseDelayMenuItem.view = maxUnpauseDelayView; [prefsMenu addItem:maxUnpauseDelayMenuItem]; // Initialize the delay sliders with current values and targets [self initDelaySliders]; } - (void) initDelaySliders { // Configure pause delay slider with logarithmic scale (0ms to 10000ms) // Slider range 0-100 maps logarithmically to 0-10000ms pauseDelaySlider.minValue = 0; pauseDelaySlider.maxValue = 100; pauseDelaySlider.integerValue = [self msToSliderValue:userDefaults.pauseDelayMS]; pauseDelaySlider.target = self; pauseDelaySlider.action = @selector(pauseDelaySliderChanged:); // Configure max unpause delay slider with logarithmic scale (0ms to 10000ms) // Slider range 0-100 maps logarithmically to 0-10000ms maxUnpauseDelaySlider.minValue = 0; maxUnpauseDelaySlider.maxValue = 100; maxUnpauseDelaySlider.integerValue = [self msToSliderValue:userDefaults.maxUnpauseDelayMS]; maxUnpauseDelaySlider.target = self; maxUnpauseDelaySlider.action = @selector(maxUnpauseDelaySliderChanged:); // Update labels with current values [self updatePauseDelayLabel]; [self updateMaxUnpauseDelayLabel]; } - (void) pauseDelaySliderChanged:(NSSlider*)sender { NSUInteger delayMS = [self sliderValueToMS:sender.integerValue]; userDefaults.pauseDelayMS = delayMS; [self updatePauseDelayLabel]; } - (void) maxUnpauseDelaySliderChanged:(NSSlider*)sender { NSUInteger delayMS = [self sliderValueToMS:sender.integerValue]; userDefaults.maxUnpauseDelayMS = delayMS; [self updateMaxUnpauseDelayLabel]; } - (void) updatePauseDelayLabel { NSUInteger delayMS = userDefaults.pauseDelayMS; if (delayMS == 0) { pauseDelayLabel.stringValue = @"Off"; } else { pauseDelayLabel.stringValue = [NSString stringWithFormat:@"%lums", (unsigned long)delayMS]; } } - (void) updateMaxUnpauseDelayLabel { NSUInteger delayMS = userDefaults.maxUnpauseDelayMS; if (delayMS == 0) { maxUnpauseDelayLabel.stringValue = @"Off"; } else { maxUnpauseDelayLabel.stringValue = [NSString stringWithFormat:@"%lums", (unsigned long)delayMS]; } } // Logarithmic conversion methods for better control at small values // Maps slider value (0-100) to milliseconds (0-10000) logarithmically - (NSUInteger) sliderValueToMS:(NSInteger)sliderValue { if (sliderValue <= 0) { return 0; } // Logarithmic mapping: slider 0-100 -> ms 0-10000 // Formula: ms = (10^(sliderValue/50) - 1) * (10000/99) // This gives finer control at small values and coarser control at large values double normalizedSlider = (double)sliderValue / 100.0; // 0-1 double logValue = pow(10.0, normalizedSlider * 2.0) - 1.0; // 0-99 NSUInteger ms = (NSUInteger)(logValue * (10000.0 / 99.0)); return MIN(ms, 10000); // Cap at 10000ms } // Maps milliseconds (0-10000) to slider value (0-100) logarithmically - (NSInteger) msToSliderValue:(NSUInteger)ms { if (ms == 0) { return 0; } // Inverse of the logarithmic mapping // Formula: sliderValue = 50 * log10((ms * 99/10000) + 1) double normalizedMS = (double)ms / 10000.0; // 0-1 double logInput = (normalizedMS * 99.0) + 1.0; // 1-100 double sliderValue = (log10(logInput) / 2.0) * 100.0; // 0-100 return (NSInteger)MIN(MAX(sliderValue, 0), 100); } @end NS_ASSUME_NONNULL_END ================================================ FILE: BGMApp/BGMApp/Scripting/BGMASApplication.h ================================================ // 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 . // // BGMASApplication.h // BGMApp // // Copyright © 2021 Marcus Wu // Copyright © 2021 Kyle Neideck // // An AppleScript class for volume and pan settings for running applications. // // Local Includes #import "BGMAppVolumesController.h" // System Includes #import #import NS_ASSUME_NONNULL_BEGIN @interface BGMASApplication : NSObject - (instancetype) initWithApplication:(NSRunningApplication*)app volumeController:(BGMAppVolumesController*)volumeController parentSpecifier:(NSScriptObjectSpecifier* __nullable)parentSpecifier index:(int)i; @property (readonly) NSString* name; @property (readonly) NSString* bundleID; @property int volume; @property int pan; @end NS_ASSUME_NONNULL_END ================================================ FILE: BGMApp/BGMApp/Scripting/BGMASApplication.m ================================================ // 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 . // // BGMASApplication.m // BGMApp // // Copyright © 2021 Marcus Wu // Copyright © 2021 Kyle Neideck // // Self Include #import "BGMASApplication.h" // Local Includes #import "BGM_Types.h" @implementation BGMASApplication { NSScriptObjectSpecifier* parentSpecifier; NSRunningApplication *application; BGMAppVolumesController* appVolumesController; int index; } - (instancetype) initWithApplication:(NSRunningApplication*)app volumeController:(BGMAppVolumesController*)volumeController parentSpecifier:(NSScriptObjectSpecifier* __nullable)parent index:(int)i { if ((self = [super init])) { parentSpecifier = parent; application = app; appVolumesController = volumeController; index = i; } return self; } - (NSString*) name { return [NSString stringWithFormat:@"%@", [application localizedName]]; } - (NSString*) bundleID { return [NSString stringWithFormat:@"%@", [application bundleIdentifier]]; } - (int) volume { return [appVolumesController getVolumeAndPanForApp:application].volume; } - (void) setVolume:(int)vol { BGMAppVolumeAndPan volume = { .volume = vol, .pan = kAppPanNoValue }; [appVolumesController setVolumeAndPan:volume forApp:application]; } - (int) pan { return [appVolumesController getVolumeAndPanForApp:application].pan; } - (void) setPan:(int)pan { BGMAppVolumeAndPan thePan = { .volume = -1, .pan = pan }; [appVolumesController setVolumeAndPan:thePan forApp:application]; } - (NSScriptObjectSpecifier* __nullable) objectSpecifier { NSScriptClassDescription* parentClassDescription = [parentSpecifier keyClassDescription]; return [[NSNameSpecifier alloc] initWithContainerClassDescription:parentClassDescription containerSpecifier:parentSpecifier key:@"applications" name:self.name]; } @end ================================================ FILE: BGMApp/BGMApp/Scripting/BGMASOutputDevice.h ================================================ // 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 . // // BGMASOutputDevice.h // BGMApp // // Copyright © 2017 Kyle Neideck // // An AppleScript class for the output devices that can be selected in the preferences menu. // // Local Includes #import "BGMAudioDeviceManager.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMASOutputDevice : NSObject - (instancetype) initWithAudioObjectID:(AudioObjectID)objID audioDevices:(BGMAudioDeviceManager*)devices parentSpecifier:(NSScriptObjectSpecifier* __nullable)parentSpecifier; @property (readonly) NSString* name; @property BOOL selected; // is this the device to be used for audio output? @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Scripting/BGMASOutputDevice.mm ================================================ // 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 . // // BGMASOutputDevice.mm // BGMApp // // Copyright © 2017 Kyle Neideck // // Self Include #import "BGMASOutputDevice.h" // Local Includes #import "BGMAudioDevice.h" // PublicUtility Includes #import "CADebugMacros.h" #pragma clang assume_nonnull begin @implementation BGMASOutputDevice { NSScriptObjectSpecifier* parentSpecifier; BGMAudioDevice device; BGMAudioDeviceManager* audioDevices; } - (instancetype) initWithAudioObjectID:(AudioObjectID)objID audioDevices:(BGMAudioDeviceManager*)devices parentSpecifier:(NSScriptObjectSpecifier* __nullable)parent { if ((self = [super init])) { parentSpecifier = parent; device = objID; audioDevices = devices; } return self; } - (NSString*) name { return (NSString*)CFBridgingRelease(device.CopyName()); } - (BOOL) selected { return [audioDevices isOutputDevice:device]; } - (void) setSelected:(BOOL)selected { if (selected && ![self selected]) { DebugMsg("BGMASOutputDevice::setSelected: A script is setting output device to %s", [[self name] UTF8String]); NSError* err = [audioDevices setOutputDeviceWithID:device revertOnFailure:YES]; (void)err; // TODO: Return an error to the script somehow if this isn't nil. Also, should // we return an error if the script tries to set this property to false? } } - (NSScriptObjectSpecifier* __nullable) objectSpecifier { NSScriptClassDescription* parentClassDescription = [parentSpecifier keyClassDescription]; return [[NSNameSpecifier alloc] initWithContainerClassDescription:parentClassDescription containerSpecifier:parentSpecifier key:@"output devices" name:self.name]; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Scripting/BGMApp.sdef ================================================ ================================================ FILE: BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.h ================================================ // 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 . // // BGMAppDelegate+AppleScript.h // BGMApp // // Copyright © 2017 Kyle Neideck // Copyright © 2021 Marcus Wu // #import "BGMAppDelegate.h" // Local Includes #import "BGMAudioDeviceManager.h" #import "BGMAppVolumesController.h" // Local Includes #import "BGMASOutputDevice.h" #import "BGMASApplication.h" // System Includes #import #pragma clang assume_nonnull begin @interface BGMAppDelegate (AppleScript) - (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key; @property BGMASOutputDevice* selectedOutputDevice; @property (readonly) NSArray* outputDevices; @property double mainVolume; @property (readonly) NSArray* applications; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/Scripting/BGMAppDelegate+AppleScript.mm ================================================ // 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 . // // BGMAppDelegate+AppleScript.mm // BGMApp // // Copyright © 2017 Kyle Neideck // Copyright © 2021 Marcus Wu // // Self Include #import "BGMAppDelegate+AppleScript.h" // Local Includes #import "BGMAudioDevice.h" // PublicUtility Includes #import "CAHALAudioSystemObject.h" #import "CAAutoDisposer.h" const AudioObjectPropertyScope kScope = kAudioDevicePropertyScopeOutput; #pragma clang assume_nonnull begin @implementation BGMAppDelegate (AppleScript) - (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key { #pragma unused (sender) if (![key isEqual:@"_keyWindow"]) { DebugMsg("BGMAppDelegate:application:delegateHandlesKey: Key queried: '%s'", [key UTF8String]); } return [@[@"selectedOutputDevice", @"outputDevices", @"mainVolume", @"applications"] containsObject:key]; } - (BGMASOutputDevice*) selectedOutputDevice { AudioObjectID outputDeviceID = [self.audioDevices outputDevice].GetObjectID(); return [[BGMASOutputDevice alloc] initWithAudioObjectID:outputDeviceID audioDevices:self.audioDevices parentSpecifier:[self objectSpecifier]]; } - (void) setSelectedOutputDevice:(BGMASOutputDevice*)device { [device setSelected:YES]; } - (NSArray*) outputDevices { UInt32 numDevices = CAHALAudioSystemObject().GetNumberAudioDevices(); CAAutoArrayDelete devices(numDevices); CAHALAudioSystemObject().GetAudioDevices(numDevices, devices); NSMutableArray* outputDevices = [NSMutableArray arrayWithCapacity:numDevices]; for (UInt32 i = 0; i < numDevices; i++) { BGMAudioDevice device(devices[i]); if (device.CanBeOutputDeviceInBGMApp()) { BGMASOutputDevice* outputDevice = [[BGMASOutputDevice alloc] initWithAudioObjectID:device.GetObjectID() audioDevices:self.audioDevices parentSpecifier:[self objectSpecifier]]; [outputDevices addObject:outputDevice]; } } return outputDevices; } - (double) mainVolume { BGMAudioDevice bgmDevice = [self.audioDevices bgmDevice]; return bgmDevice.GetVolumeControlScalarValue(kScope, kMasterChannel); } - (void) setMainVolume:(double)mainVolume { BGMAudioDevice bgmDevice = [self.audioDevices bgmDevice]; bgmDevice.SetMasterVolumeScalar(kScope, (Float32)mainVolume); [self.outputVolumeSlider setFloatValue:(float)mainVolume]; } - (NSArray*) applications { NSArray* apps = [[NSWorkspace sharedWorkspace] runningApplications]; NSMutableArray* applications = [NSMutableArray arrayWithCapacity:[apps count]]; for (UInt32 i = 0; i < [apps count]; i++) { BGMASApplication *app = [[BGMASApplication alloc] initWithApplication:apps[i] volumeController:self.appVolumes parentSpecifier:[self objectSpecifier] index:i]; [applications addObject:app]; } return applications; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMApp/SystemPreferences.h ================================================ /* * SystemPreferences.h */ #import #import @class SystemPreferencesItem, SystemPreferencesApplication, SystemPreferencesColor, SystemPreferencesDocument, SystemPreferencesWindow, SystemPreferencesAttributeRun, SystemPreferencesCharacter, SystemPreferencesParagraph, SystemPreferencesText, SystemPreferencesAttachment, SystemPreferencesWord, SystemPreferencesAnchor, SystemPreferencesPane, SystemPreferencesPrintSettings; enum SystemPreferencesSavo { SystemPreferencesSavoAsk = 'ask ' /* Ask the user whether or not to save the file. */, SystemPreferencesSavoNo = 'no ' /* Do not save the file. */, SystemPreferencesSavoYes = 'yes ' /* Save the file. */ }; typedef enum SystemPreferencesSavo SystemPreferencesSavo; enum SystemPreferencesEnum { SystemPreferencesEnumStandard = 'lwst' /* Standard PostScript error handling */, SystemPreferencesEnumDetailed = 'lwdt' /* print a detailed report of PostScript errors */ }; typedef enum SystemPreferencesEnum SystemPreferencesEnum; /* * Standard Suite */ // A scriptable object. @interface SystemPreferencesItem : SBObject @property (copy) NSDictionary *properties; // All of the object's properties. - (void)closeSaving:(SystemPreferencesSavo) saving savingIn:(NSURL *)savingIn; // Close an object. - (void)delete; // Delete an object. - (void)duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location. - (BOOL)exists; // Verify if an object exists. - (void)moveTo:(SBObject *)to; // Move object(s) to a new location. - (void)saveAs:(NSString *)as in:(NSURL *)in_; // Save an object. @end // An application's top level scripting object. @interface SystemPreferencesApplication : SBApplication - (SBElementArray *)documents; - (SBElementArray *)windows; @property (readonly) BOOL frontmost; // Is this the frontmost (active) application? @property (copy, readonly) NSString *name; // The name of the application. @property (copy, readonly) NSString *version; // The version of the application. - (SystemPreferencesDocument *)open:(NSURL *)x; // Open an object. - (void)print:(NSURL *)x printDialog:(BOOL) printDialog withProperties:(SystemPreferencesPrintSettings *)withProperties; // Print an object. - (void)quitSaving:(SystemPreferencesSavo)saving; // Quit an application. @end // A color. @interface SystemPreferencesColor : SystemPreferencesItem @end // A document. @interface SystemPreferencesDocument : SystemPreferencesItem @property (readonly) BOOL modified; // Has the document been modified since the last save? @property (copy) NSString *name; // The document's name. @property (copy) NSString *path; // The document's path. @end // A window. @interface SystemPreferencesWindow : SystemPreferencesItem @property NSRect bounds; // The bounding rectangle of the window. @property (readonly) BOOL closeable; // Whether the window has a close box. @property (copy, readonly) SystemPreferencesDocument *document; // The document whose contents are being displayed in the window. @property (readonly) BOOL floating; // Whether the window floats. - (NSInteger)id; // The unique identifier of the window. @property NSInteger index; // The index of the window, ordered front to back. @property (readonly) BOOL miniaturizable; // Whether the window can be miniaturized. @property BOOL miniaturized; // Whether the window is currently miniaturized. @property (readonly) BOOL modal; // Whether the window is the application's current modal window. @property (copy) NSString *name; // The full title of the window. @property (readonly) BOOL resizable; // Whether the window can be resized. @property (readonly) BOOL titled; // Whether the window has a title bar. @property BOOL visible; // Whether the window is currently visible. @property (readonly) BOOL zoomable; // Whether the window can be zoomed. @property BOOL zoomed; // Whether the window is currently zoomed. @end /* * Text Suite */ // This subdivides the text into chunks that all have the same attributes. @interface SystemPreferencesAttributeRun : SystemPreferencesItem - (SBElementArray *)attachments; - (SBElementArray *)attributeRuns; - (SBElementArray *)characters; - (SBElementArray *)paragraphs; - (SBElementArray *)words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // This subdivides the text into characters. @interface SystemPreferencesCharacter : SystemPreferencesItem - (SBElementArray *)attachments; - (SBElementArray *)attributeRuns; - (SBElementArray *)characters; - (SBElementArray *)paragraphs; - (SBElementArray *)words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // This subdivides the text into paragraphs. @interface SystemPreferencesParagraph : SystemPreferencesItem - (SBElementArray *)attachments; - (SBElementArray *)attributeRuns; - (SBElementArray *)characters; - (SBElementArray *)paragraphs; - (SBElementArray *)words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // Rich (styled) text @interface SystemPreferencesText : SystemPreferencesItem - (SBElementArray *)attachments; - (SBElementArray *)attributeRuns; - (SBElementArray *)characters; - (SBElementArray *)paragraphs; - (SBElementArray *)words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end // Represents an inline text attachment. This class is used mainly for make commands. @interface SystemPreferencesAttachment : SystemPreferencesText @property (copy) NSString *fileName; // The path to the file for the attachment @end // This subdivides the text into words. @interface SystemPreferencesWord : SystemPreferencesItem - (SBElementArray *)attachments; - (SBElementArray *)attributeRuns; - (SBElementArray *)characters; - (SBElementArray *)paragraphs; - (SBElementArray *)words; @property (copy) NSColor *color; // The color of the first character. @property (copy) NSString *font; // The name of the font of the first character. @property NSInteger size; // The size in points of the first character. @end /* * System Preferences */ // an anchor within a preference pane @interface SystemPreferencesAnchor : SystemPreferencesItem @property (copy, readonly) NSString *name; // name of the anchor within a preference pane - (SystemPreferencesAnchor *)reveal; // Reveals an anchor within a preference pane or preference pane itself @end // System Preferences top level scripting object @interface SystemPreferencesApplication (SystemPreferences) - (SBElementArray *)panes; @property (copy) SystemPreferencesPane *currentPane; // the currently selected pane @property (copy, readonly) SystemPreferencesWindow *preferencesWindow; // the main preferences window @property BOOL showAll; // Is SystemPrefs in show all view. (Setting to false will do nothing) @end // a preference pane @interface SystemPreferencesPane : SystemPreferencesItem - (SBElementArray *)anchors; - (NSString *)id; // locale independent name of the preference pane; can refer to a pane using the expression: pane id "" @property (copy, readonly) NSString *localizedName; // localized name of the preference pane @property (copy, readonly) NSString *name; // name of the preference pane as it appears in the title bar; can refer to a pane using the expression: pane "" - (NSInteger)timedLoad; // This command does xxxx. @end /* * Type Definitions */ @interface SystemPreferencesPrintSettings : SBObject @property NSInteger copies; // the number of copies of a document to be printed @property BOOL collating; // Should printed copies be collated? @property NSInteger startingPage; // the first page of the document to be printed @property NSInteger endingPage; // the last page of the document to be printed @property NSInteger pagesAcross; // number of logical pages laid across a physical page @property NSInteger pagesDown; // number of logical pages laid out down a physical page @property (copy) NSDate *requestedPrintTime; // the time at which the desktop printer should print the document @property SystemPreferencesEnum errorHandling; // how errors are handled @property (copy) NSString *faxNumber; // for fax number @property (copy) NSString *targetPrinter; // for target printer - (void)closeSaving:(SystemPreferencesSavo) saving savingIn:(NSURL *)savingIn; // Close an object. - (void)delete; // Delete an object. - (void)duplicateTo:(SBObject *)to withProperties:(NSDictionary *)withProperties; // Copy object(s) and put the copies at a new location. - (BOOL)exists; // Verify if an object exists. - (void)moveTo:(SBObject *)to; // Move object(s) to a new location. - (void)saveAs:(NSString *)as in:(NSURL *)in_; // Save an object. @end ================================================ FILE: BGMApp/BGMApp/_uninstall-non-interactive.sh ================================================ #!/bin/bash # vim: tw=120: # 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 . # # _uninstall-non-interactive.sh # # Copyright © 2016 Nick Jacques # Copyright © 2016, 2017, 2021 Kyle Neideck # # Removes BGMApp, BGMDriver and BGMXPCHelper from the system immediately. Run by uninstall.sh and the Homebrew formula. # # TODO: Log commands and their output to uninstall.log, like build_and_install.sh does, rather than just sending # everything to /dev/null. # TODO: Show a custom error message if the script fails, like build_and_install.sh. # Halt on errors. set -e PATH=/bin:/sbin:/usr/bin:/usr/sbin; export PATH bold=$(tput bold) normal=$(tput sgr0) app_path="/Applications/Background Music.app" driver_path="/Library/Audio/Plug-Ins/HAL/Background Music Device.driver" xpc_path1="/usr/local/libexec/BGMXPCHelper.xpc" xpc_path2="/Library/Application Support/Background Music/BGMXPCHelper.xpc" # Check that files/directories are at most this big before we delete them, just to be safe. Note that the bundles can # include debug symbols, e.g. if you use build_and_install.sh, which makes them a lot bigger. max_size_mb_for_rm=30 file_paths=("${app_path}" "${driver_path}" "${xpc_path1}" "${xpc_path2}") bgmapp_process_name="Background Music" launchd_plist_label="com.bearisdriving.BGM.XPCHelper" launchd_plist="/Library/LaunchDaemons/${launchd_plist_label}.plist" coreaudiod_plist="/System/Library/LaunchDaemons/com.apple.audio.coreaudiod.plist" user_group_name="_BGMXPCHelper" # We move files to this temp directory and then move the directory to the user's trash at the end of the script. # Unfortunately, this means that if the user tries to use the "put back" feature the files will just go back to the # temp directory. trash_dir="$(mktemp -d -t UninstalledBackgroundMusicFiles)/" # Takes a path to a file or directory and returns false if the file/directory is larger than $max_size_mb_for_rm. function size_check { local size="$(du -sm "$1" 2>/dev/null | awk '{ print $1 }')" [[ "${size}" =~ ^[0-9]+$ ]] && [[ "${size}" -le ${max_size_mb_for_rm} ]] } # Ensure that the user can use sudo. (Use `sudo true` instead of `sudo -v` because that causes a # password prompt in Travis CI builds for some reason.) if ! sudo true; then echo "ERROR: This script must be run by a user with administrator (sudo) privileges." >&2 exit 1 fi # Try to kill Background Music.app, in case it's running. killall "${bgmapp_process_name}" &>/dev/null || true # TODO: Use # mdfind kMDItemCFBundleIdentifier = "com.bearisdriving.BGM.App" # to offer alternatives if Background Music.app isn't installed to /Applications. Or we could open it with # open -b "com.bearisdriving.BGM.App" -- delete-yourself # and have Background Music.app delete itself and close when it gets the "delete-yourself" argument. Though # that wouldn't be backwards compatible. # Remove the files defined in file_paths for path in "${file_paths[@]}"; do if [ -e "${path}" ]; then if size_check "${path}"; then echo "Moving \"${path}\" to the trash." sudo mv -f "${path}" "${trash_dir}" &>/dev/null else echo "Error: Refusing to delete \"${path}\" because it was much larger than expected." >&2 fi fi done echo "Removing Background Music launchd service." sudo launchctl list | grep "${launchd_plist_label}" >/dev/null && \ (sudo launchctl bootout system "${launchd_plist}" &>/dev/null || \ # Try older versions of the command in case the user has an old version of launchctl. sudo launchctl unbootstrap system "${launchd_plist}" &>/dev/null || \ sudo launchctl unload "${launchd_plist}" >/dev/null) || \ echo " Service does not exist." echo "Removing Background Music launchd service configuration file." if [ -e "${launchd_plist}" ]; then sudo mv -f "${launchd_plist}" "${trash_dir}" fi # Be paranoid about user_group_name because we really don't want to delete every user account. if ! [[ -z ${user_group_name} ]] && [[ "${user_group_name}" != "" ]]; then echo "Removing Background Music user." dscl . -read /Users/"${user_group_name}" &>/dev/null && \ sudo dscl . -delete /Users/"${user_group_name}" 1>/dev/null || \ echo " User does not exist." echo "Removing Background Music group." dscl . -read /Groups/"${user_group_name}" &>/dev/null && \ sudo dscl . -delete /Groups/"${user_group_name}" 1>/dev/null || \ echo " Group does not exist." else echo "Warning: could not delete the Background Music user/group due to an internal error in $0." >&2 fi # We're done removing files, so now actually move trash_dir into the trash. And if that fails, just delete it normally. osascript -e 'tell application id "com.apple.finder" move the POSIX file "'"${trash_dir}"'" to trash end tell' >/dev/null 2>&1 \ || rm -rf "${trash_dir}" \ || true echo "Restarting Core Audio." # Wait a little because moving files to the trash plays a short sound. sleep 2 # The extra or-clauses are fallback versions of the command that restarts coreaudiod. Apparently some of these commands # don't work with older versions of launchctl, so I figure there's no harm in trying a bunch of different ways until # one works. (sudo launchctl kickstart -k system/com.apple.audio.coreaudiod &>/dev/null || \ sudo killall coreaudiod &>/dev/null || \ sudo launchctl kill SIGTERM system/com.apple.audio.coreaudiod &>/dev/null || \ sudo launchctl kill TERM system/com.apple.audio.coreaudiod &>/dev/null || \ sudo launchctl kill 15 system/com.apple.audio.coreaudiod &>/dev/null || \ sudo launchctl kill -15 system/com.apple.audio.coreaudiod &>/dev/null || \ (sudo launchctl unload "${coreaudiod_plist}" &>/dev/null && \ sudo launchctl load "${coreaudiod_plist}" &>/dev/null)) echo "..." sleep 3 # TODO: What if they only have one audio device? echo "" echo "${bold}Done! Toggle your audio output device in the Sound section of System Settings to finish" \ "uninstalling. (Or just restart your computer.)${normal}" ================================================ FILE: BGMApp/BGMApp/main.m ================================================ // 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 . // // main.m // BGMApp // // Copyright © 2016 Kyle Neideck // // System Includes #import int main(int argc, const char * argv[]) { // Start BGMApp. return NSApplicationMain(argc, argv); } ================================================ FILE: BGMApp/BGMApp.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 47; objects = { /* Begin PBXBuildFile section */ 19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; }; 19FE70F73D26D54450779A22 /* BGMPlayThroughRTLogger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7DE5E3BA0046ED2BC3C6 /* BGMPlayThroughRTLogger.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPlayThroughRTLogger.cpp"; }; }; 19FE715E7338035C7BCD24E7 /* BGMPlayThroughRTLogger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7DE5E3BA0046ED2BC3C6 /* BGMPlayThroughRTLogger.cpp */; }; 19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVolumeChangeListener.cpp"; }; }; 19FE72566BCEB11BD1F3D487 /* BGMMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73822ADD50BA9120AB05 /* BGMMusic.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMMusic.m"; }; }; 19FE72D66CBC5C39F86333DE /* BGMPlayThroughRTLogger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7DE5E3BA0046ED2BC3C6 /* BGMPlayThroughRTLogger.cpp */; }; 19FE734C861E0370C21E4E94 /* BGMDebugLogging.c in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73389459BF65748F531F /* BGMDebugLogging.c */; }; 19FE7590D7565E7677D84C55 /* BGMDebugLogging.c in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73389459BF65748F531F /* BGMDebugLogging.c */; }; 19FE76F614F260F3F65AF550 /* BGMMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73822ADD50BA9120AB05 /* BGMMusic.m */; }; 19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; }; 19FE78EEC6D3C3B19D1FBD64 /* BGMDebugLogging.c in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73389459BF65748F531F /* BGMDebugLogging.c */; }; 19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; }; 19FE7B32E1214BA0E8166A9E /* BGMMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73822ADD50BA9120AB05 /* BGMMusic.m */; }; 19FE7B7BDF0C683288654F90 /* BGMDebugLogging.c in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73389459BF65748F531F /* BGMDebugLogging.c */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDebugLogging.c"; }; }; 19FE7BD48C0CA2CAF16C9ACE /* BGMPlayThroughTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE761D0371DEF9FDF053D6 /* BGMPlayThroughTests.mm */; }; 19FE7C144C12607D947EB030 /* BGMDebugLogging.c in Sources */ = {isa = PBXBuildFile; fileRef = 19FE73389459BF65748F531F /* BGMDebugLogging.c */; }; 19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */; }; 19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMStatusBarItem.mm"; }; }; 1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusicPrefs.mm"; }; }; 1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferencesMenu.mm"; }; }; 1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMusic.mm"; }; }; 1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CARingBuffer.cpp"; }; }; 1C1962E71BC94E91008A4DF7 /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPlayThrough.cpp"; }; }; 1C1962F31BCABFC5008A4DF7 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioDevice.cpp"; }; }; 1C1962F41BCABFC5008A4DF7 /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioObject.cpp"; }; }; 1C1962F51BCABFC5008A4DF7 /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioStream.cpp"; }; }; 1C1962F61BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHALAudioSystemObject.cpp"; }; }; 1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CADebugMacros.cpp"; }; }; 1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CADebugPrintf.cpp"; }; }; 1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFString.cpp"; }; }; 1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */; }; 1C1963061BCAF468008A4DF7 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAMutex.cpp"; }; }; 1C1963091BCAF677008A4DF7 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAHostTimeBase.cpp"; }; }; 1C1AA4B01F9C673000BCFB22 /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGM_Utils.cpp"; }; }; 1C1AA4B11F9DE3B700BCFB22 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMAudioDevice.cpp"; }; }; 1C1AA4B21F9DE3B700BCFB22 /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMBackgroundMusicDevice.cpp"; }; }; 1C1AA4B31F9DE40000BCFB22 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; }; 1C227C0B1FA4C48200A95B6D /* BGMAppVolumes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */; }; 1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMMusicPlayer.m"; }; }; 1C2336DF1BEAE10C004C1C4E /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSpotify.m"; }; }; 1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */; }; 1C2FC3141EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppDelegate+AppleScript.mm"; }; }; 1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; }; 1C2FC31B1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMASOutputDevice.mm"; }; }; 1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; }; 1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDeviceControlsList.cpp"; }; }; 1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; }; 1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; }; 1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppVolumes.m"; }; }; 1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMiTunes.m"; }; }; 1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDeviceControlSync.cpp"; }; }; 1C4D1A1D217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMPreferredOutputDevices.mm"; }; }; 1C4D1A1E217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */; }; 1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; }; 1C533C7A1EED28B700270802 /* uninstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C533C791EED28B700270802 /* uninstall.sh */; }; 1C533C7B1EED2F6200270802 /* safe_install_dir.sh in Resources */ = {isa = PBXBuildFile; fileRef = 276972901CB16008007A2F7C /* safe_install_dir.sh */; }; 1C533C7C1EED2F8A00270802 /* com.bearisdriving.BGM.XPCHelper.plist.template in Resources */ = {isa = PBXBuildFile; fileRef = 2769728D1CAFCEFD007A2F7C /* com.bearisdriving.BGM.XPCHelper.plist.template */; }; 1C533C801EF532CA00270802 /* _uninstall-non-interactive.sh in Resources */ = {isa = PBXBuildFile; fileRef = 1C533C7F1EF532CA00270802 /* _uninstall-non-interactive.sh */; }; 1C62FE4E23D3EB2E00B9B68E /* MockAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C62FE4523D3EB2D00B9B68E /* MockAudioObject.cpp */; }; 1C62FE4F23D3EB2E00B9B68E /* Mock_CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C62FE4623D3EB2D00B9B68E /* Mock_CAHALAudioObject.cpp */; }; 1C62FE5023D3EB2E00B9B68E /* MockAudioObjects.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C62FE4723D3EB2D00B9B68E /* MockAudioObjects.cpp */; }; 1C62FE5123D3EB2E00B9B68E /* Mock_CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C62FE4823D3EB2D00B9B68E /* Mock_CAHALAudioSystemObject.cpp */; }; 1C62FE5223D3EB2E00B9B68E /* Mock_CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C62FE4A23D3EB2E00B9B68E /* Mock_CAHALAudioDevice.cpp */; }; 1C62FE5323D3EB2E00B9B68E /* MockAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C62FE4B23D3EB2E00B9B68E /* MockAudioDevice.cpp */; }; 1C62FE5523D423D700B9B68E /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C62FE5423D423D700B9B68E /* XCTest.framework */; }; 1C62FE5823D4278300B9B68E /* skip-ui-tests.py in Resources */ = {isa = PBXBuildFile; fileRef = 1C62FE5623D4278300B9B68E /* skip-ui-tests.py */; }; 1C687A6B23B889E000834B75 /* BGMPlayThroughRTLoggerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C687A6A23B889E000834B75 /* BGMPlayThroughRTLoggerTests.mm */; }; 1C780FF21FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSystemSoundsVolume.mm"; }; }; 1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */; }; 1C8034D520B0347A004BC50C /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8034D420B0347A004BC50C /* Security.framework */; }; 1C80DED320A6718600045BBE /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppWatcher.m"; }; }; 1C80DED420A6718600045BBE /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; }; 1C8104AF22AD07E200B35517 /* BGMAppWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C80DED220A6718600045BBE /* BGMAppWatcher.m */; }; 1C8104B022AD082E00B35517 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; }; 1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMOutputVolumeMenuItem.mm"; }; }; 1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; }; 1C837DDA1F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */; }; 1C86DA6A1F91EE3B000C8CCF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CAPThread.cpp"; }; }; 1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8B0C69216205BF008C5679 /* AVFoundation.framework */; }; 1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C8B0C69216205BF008C5679 /* AVFoundation.framework */; }; 1C8D8304204238DB00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMSwinsian.m"; }; }; 1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; }; 1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */; }; 1C8D830B2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMGooglePlayMusicDesktopPlayer.m"; }; }; 1C8D830C2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */; }; 1C8D830E2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */ = {isa = PBXBuildFile; fileRef = 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */; }; 1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */ = {isa = PBXBuildFile; fileRef = 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */; }; 1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMGooglePlayMusicDesktopPlayerConnection.m"; }; }; 1C9258482090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; }; 1C9258492090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */; }; 1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMBackgroundMusicDevice.cpp"; }; }; 1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; }; 1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */; }; 1CB8B33D1BBA75EF000E2DD1 /* BGMAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppDelegate.mm"; }; }; 1CB8B33F1BBA75EF000E2DD1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33E1BBA75EF000E2DD1 /* main.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-main.m"; }; }; 1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFArray.cpp"; }; }; 1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFDictionary.cpp"; }; }; 1CC1DF911BE5891300FB8FE4 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CADebugger.cpp"; }; }; 1CC1DF961BE8607700FB8FE4 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1CC1DF951BE8607700FB8FE4 /* Images.xcassets */; }; 1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMTermination.mm"; }; }; 1CC6593D1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; }; 1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */; }; 1CCC4F3E1E58196C008053E4 /* BGMXPCHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F3C1E58196C008053E4 /* BGMXPCHelperTests.m */; }; 1CCC4F4D1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F4B1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm */; }; 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 */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAppVolumesController.mm"; }; }; 1CD410D51F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */; }; 1CD410D61F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */; }; 1CD989341ECFFC9E0014BBBF /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; }; 1CD989351ECFFC9E0014BBBF /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; }; 1CD989361ECFFC9E0014BBBF /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; }; 1CD989371ECFFC9E0014BBBF /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; }; 1CD989381ECFFC9E0014BBBF /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; }; 1CD989391ECFFC9E0014BBBF /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; }; 1CD9893A1ECFFC9E0014BBBF /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; }; 1CD9893B1ECFFC9E0014BBBF /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; }; 1CD9893C1ECFFC9E0014BBBF /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; }; 1CD9893D1ECFFC9E0014BBBF /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; }; 1CD9893E1ECFFC9E0014BBBF /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; }; 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.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 */; }; 1CD989461ECFFCFC0014BBBF /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; }; 1CD989471ECFFCFC0014BBBF /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; }; 1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; }; 1CD989491ECFFCFC0014BBBF /* BGMDecibel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F7D48F1D2483B100821C4B /* BGMDecibel.m */; }; 1CD9894A1ECFFCFC0014BBBF /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; }; 1CD9894B1ECFFCFC0014BBBF /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; }; 1CD9894C1ECFFCFC0014BBBF /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; }; 1CD9894D1ECFFCFC0014BBBF /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; }; 1CD9894E1ECFFCFC0014BBBF /* BGMVOX.m in Sources */ = {isa = PBXBuildFile; fileRef = 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */; }; 1CD9894F1ECFFCFC0014BBBF /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; }; 1CD989501ECFFCFC0014BBBF /* BGMAboutPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */; }; 1CD989511ECFFCFC0014BBBF /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; }; 1CD989521ECFFCFC0014BBBF /* BGMOutputDeviceMenuSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */; }; 1CD989531ECFFCFC0014BBBF /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; }; 1CD989541ECFFCFC0014BBBF /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; }; 1CD989551ECFFCFC0014BBBF /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; }; 1CD989561ECFFCFC0014BBBF /* BGMXPCListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */; }; 1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; }; 1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; }; 1CD989591ECFFD250014BBBF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; }; 1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; }; 1CE03A57239B56740036908D /* BGMDebugLoggingMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CE03A56239B56740036908D /* BGMDebugLoggingMenuItem.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDebugLoggingMenuItem.m"; }; }; 1CE03A58239B56740036908D /* BGMDebugLoggingMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CE03A56239B56740036908D /* BGMDebugLoggingMenuItem.m */; }; 1CE03A59239B56740036908D /* BGMDebugLoggingMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CE03A56239B56740036908D /* BGMDebugLoggingMenuItem.m */; }; 1CE7064C1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMOutputDeviceMenuSection.mm"; }; }; 1CED61691C3081C2002CAFCF /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 1CED61681C3081C2002CAFCF /* LICENSE */; }; 1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAudioDeviceManager.mm"; }; }; 1CF2D58F1F944773008B6E35 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */; }; 1CF2D5901F944789008B6E35 /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioDevice.cpp"; }; }; 1CF2D5911F944789008B6E35 /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioObject.cpp"; }; }; 1CF2D5921F944789008B6E35 /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioSystemObject.cpp"; }; }; 1CF2D5931F94479A008B6E35 /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHALAudioStream.cpp"; }; }; 1CF2D5941F9447AE008B6E35 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFArray.cpp"; }; }; 1CF2D5951F9447AE008B6E35 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFDictionary.cpp"; }; }; 1CF2D5961F9447AE008B6E35 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFNumber.cpp"; }; }; 1CF2D5971F9447AE008B6E35 /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CACFString.cpp"; }; }; 1CF2D5981F9447AE008B6E35 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CADebugger.cpp"; }; }; 1CF2D5991F9447AE008B6E35 /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CADebugMacros.cpp"; }; }; 1CF2D59A1F9447AE008B6E35 /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CADebugPrintf.cpp"; }; }; 1CF2D59B1F9447AE008B6E35 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAHostTimeBase.cpp"; }; }; 1CF2D59C1F9447AE008B6E35 /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAMutex.cpp"; }; }; 1CF2D59D1F9447AE008B6E35 /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CAPThread.cpp"; }; }; 1CF2D59E1F9447AE008B6E35 /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-CARingBuffer.cpp"; }; }; 1CF5423C1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAudioDevice.cpp"; }; }; 1CF5423D1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; }; 270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270A84501E0044EE00F13C99 /* ScriptingBridge.framework */; }; 271677BA1C6CBDFA0080B0A2 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-CACFNumber.cpp"; }; }; 27379B8A1C7C562D0084A24C /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVLC.m"; }; }; 273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */ = {isa = PBXBuildFile; fileRef = 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMVOX.m"; }; }; 2743C9EB1D852B360089613B /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMMusicPlayers.mm"; }; }; 2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMScriptingBridge.m"; }; }; 2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMUserDefaults.m"; }; }; 2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; }; 2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; }; 2743CA031D86D41C0089613B /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; }; 2743CA041D86D41C0089613B /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; }; 2743CA051D86D41C0089613B /* BGMDecibel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F7D48F1D2483B100821C4B /* BGMDecibel.m */; }; 2743CA061D86D41C0089613B /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; }; 2743CA071D86D41C0089613B /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; }; 2743CA081D86D41C0089613B /* BGMVOX.m in Sources */ = {isa = PBXBuildFile; fileRef = 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */; }; 2743CA091D86D41C0089613B /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; }; 2743CA0A1D86D52D0089613B /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; }; 2743CA0C1D86D7FA0089613B /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; }; 2743CA0D1D86D7FA0089613B /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; }; 2743CA0E1D86D7FA0089613B /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; }; 2743CA0F1D86D7FA0089613B /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; }; 2743CA101D86D7FA0089613B /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; }; 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 */; }; 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 */; }; 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 */; }; 274827951E11052500B31D8D /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1CB8B3421BBA75EF000E2DD1 /* MainMenu.xib */; }; 277170161CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 277170151CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMXPCListenerDelegate.m"; }; }; 2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMXPCListener.mm"; }; }; 279F48771DD6D73A00768A85 /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMHermes.m"; }; }; 27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAutoPauseMenuItem.m"; }; }; 27D1D6BB1DD7226C0049E707 /* BGMAboutPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMAboutPanel.m"; }; }; 27D643C01C9FB99200737F6E /* BGMXPCHelperService.mm in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.mm */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-BGMXPCHelperService.mm"; }; }; 27D643C11C9FB99200737F6E /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D643BC1C9FB84C00737F6E /* main.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMXPCHelper-main.m"; }; }; 27F7D4901D2483B100821C4B /* BGMDecibel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F7D48F1D2483B100821C4B /* BGMDecibel.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMDecibel.m"; }; }; 27FB8C071DD75D0A0084DB9D /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; }; 27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGM_Utils.cpp"; }; }; 27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; }; 27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; }; 9E129A412602AE620005851B /* BGMASApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E129A402602AE620005851B /* BGMASApplication.m */; settings = {COMPILER_FLAGS = "-frandom-seed=BGMApp-BGMASApplication.m"; }; }; 9E542C7026057FBA0016C0B5 /* BGMASApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 9E129A402602AE620005851B /* BGMASApplication.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 1CCC4F591E584081008053E4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1CB8B32E1BBA75EF000E2DD1 /* Project object */; proxyType = 1; remoteGlobalIDString = 1CB8B3351BBA75EF000E2DD1; remoteInfo = "Background Music"; }; 278D71F31CABB88B00899CF9 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 1CB8B32E1BBA75EF000E2DD1 /* Project object */; proxyType = 1; remoteGlobalIDString = 27379B8E1C7F57DA0084A24C; remoteInfo = BGMXPCHelper; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 19FE70CF6C93F5007940CE91 /* BGMMusic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMMusic.h; path = "Music Players/BGMMusic.h"; sourceTree = ""; }; 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMVolumeChangeListener.cpp; sourceTree = ""; }; 19FE71BCD79E7246F7345C16 /* BGMThreadSafetyAnalysis.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMThreadSafetyAnalysis.h; sourceTree = ""; }; 19FE72A176FD500FB4C1F5C6 /* BGMPlayThroughRTLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMPlayThroughRTLogger.h; sourceTree = ""; }; 19FE73389459BF65748F531F /* BGMDebugLogging.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = BGMDebugLogging.c; path = PublicUtility/BGMDebugLogging.c; sourceTree = ""; }; 19FE73822ADD50BA9120AB05 /* BGMMusic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMMusic.m; path = "Music Players/BGMMusic.m"; sourceTree = ""; }; 19FE761D0371DEF9FDF053D6 /* BGMPlayThroughTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMPlayThroughTests.mm; path = UnitTests/BGMPlayThroughTests.mm; sourceTree = ""; }; 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMStatusBarItem.mm; sourceTree = ""; }; 19FE7908A33FA7BD97B432D9 /* BGMDebugLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMDebugLogging.h; path = PublicUtility/BGMDebugLogging.h; sourceTree = ""; }; 19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMStatusBarItem.h; sourceTree = ""; }; 19FE7DE5E3BA0046ED2BC3C6 /* BGMPlayThroughRTLogger.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMPlayThroughRTLogger.cpp; sourceTree = ""; }; 19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMVolumeChangeListener.h; sourceTree = ""; }; 1C09150723F010FB001EB0E1 /* set-version.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "set-version.sh"; sourceTree = ""; }; 1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMAutoPauseMusicPrefs.h; path = Preferences/BGMAutoPauseMusicPrefs.h; sourceTree = ""; }; 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMAutoPauseMusicPrefs.mm; path = Preferences/BGMAutoPauseMusicPrefs.mm; sourceTree = ""; }; 1C0BD0A61BF1B029004F4CF5 /* BGMPreferencesMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMPreferencesMenu.h; path = Preferences/BGMPreferencesMenu.h; sourceTree = ""; }; 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMPreferencesMenu.mm; path = Preferences/BGMPreferencesMenu.mm; sourceTree = ""; }; 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAutoPauseMusic.mm; sourceTree = ""; }; 1C1465B91BCC49D1003AEFE6 /* BGMAutoPauseMusic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAutoPauseMusic.h; sourceTree = ""; }; 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CARingBuffer.cpp; path = PublicUtility/CARingBuffer.cpp; sourceTree = ""; }; 1C1962E31BC94E15008A4DF7 /* CARingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CARingBuffer.h; path = PublicUtility/CARingBuffer.h; sourceTree = ""; }; 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMPlayThrough.cpp; sourceTree = ""; }; 1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMPlayThrough.h; sourceTree = ""; }; 1C1962E81BC95301008A4DF7 /* CAAtomic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAtomic.h; path = PublicUtility/CAAtomic.h; sourceTree = ""; }; 1C1962E91BC95301008A4DF7 /* CAAutoDisposer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAAutoDisposer.h; path = PublicUtility/CAAutoDisposer.h; sourceTree = ""; }; 1C1962EA1BC95301008A4DF7 /* CABitOperations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CABitOperations.h; path = PublicUtility/CABitOperations.h; sourceTree = ""; }; 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAHALAudioDevice.cpp; path = PublicUtility/CAHALAudioDevice.cpp; sourceTree = ""; }; 1C1962EC1BCABFC5008A4DF7 /* CAHALAudioDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAHALAudioDevice.h; path = PublicUtility/CAHALAudioDevice.h; sourceTree = ""; }; 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAHALAudioObject.cpp; path = PublicUtility/CAHALAudioObject.cpp; sourceTree = ""; }; 1C1962EE1BCABFC5008A4DF7 /* CAHALAudioObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAHALAudioObject.h; path = PublicUtility/CAHALAudioObject.h; sourceTree = ""; }; 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAHALAudioStream.cpp; path = PublicUtility/CAHALAudioStream.cpp; sourceTree = ""; }; 1C1962F01BCABFC5008A4DF7 /* CAHALAudioStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAHALAudioStream.h; path = PublicUtility/CAHALAudioStream.h; sourceTree = ""; }; 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAHALAudioSystemObject.cpp; path = PublicUtility/CAHALAudioSystemObject.cpp; sourceTree = ""; }; 1C1962F21BCABFC5008A4DF7 /* CAHALAudioSystemObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAHALAudioSystemObject.h; path = PublicUtility/CAHALAudioSystemObject.h; sourceTree = ""; }; 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CADebugMacros.cpp; path = PublicUtility/CADebugMacros.cpp; sourceTree = ""; }; 1C1962F81BCAC061008A4DF7 /* CADebugMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CADebugMacros.h; path = PublicUtility/CADebugMacros.h; sourceTree = ""; }; 1C1962F91BCAC061008A4DF7 /* CAPropertyAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPropertyAddress.h; path = PublicUtility/CAPropertyAddress.h; sourceTree = ""; }; 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CADebugPrintf.cpp; path = PublicUtility/CADebugPrintf.cpp; sourceTree = ""; }; 1C1962FC1BCAC0C3008A4DF7 /* CADebugPrintf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CADebugPrintf.h; path = PublicUtility/CADebugPrintf.h; sourceTree = ""; }; 1C1962FE1BCAC0DB008A4DF7 /* CAException.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = CAException.h; path = PublicUtility/CAException.h; sourceTree = ""; }; 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFString.cpp; path = PublicUtility/CACFString.cpp; sourceTree = ""; }; 1C1963001BCAC0F6008A4DF7 /* CACFString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFString.h; path = PublicUtility/CACFString.h; sourceTree = ""; }; 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = System/Library/Frameworks/CoreAudio.framework; sourceTree = SDKROOT; }; 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAMutex.cpp; path = PublicUtility/CAMutex.cpp; sourceTree = ""; }; 1C1963051BCAF468008A4DF7 /* CAMutex.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAMutex.h; path = PublicUtility/CAMutex.h; sourceTree = ""; }; 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAHostTimeBase.cpp; path = PublicUtility/CAHostTimeBase.cpp; sourceTree = ""; }; 1C1963081BCAF677008A4DF7 /* CAHostTimeBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAHostTimeBase.h; path = PublicUtility/CAHostTimeBase.h; sourceTree = ""; }; 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMMusicPlayer.m; path = "Music Players/BGMMusicPlayer.m"; sourceTree = ""; }; 1C2336DB1BEAB73F004C1C4E /* BGMiTunes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMiTunes.h; path = "Music Players/BGMiTunes.h"; sourceTree = ""; }; 1C2336DC1BEAB73F004C1C4E /* BGMMusicPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMMusicPlayer.h; path = "Music Players/BGMMusicPlayer.h"; sourceTree = ""; }; 1C2336DD1BEAE10C004C1C4E /* BGMSpotify.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMSpotify.h; path = "Music Players/BGMSpotify.h"; sourceTree = ""; }; 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMSpotify.m; path = "Music Players/BGMSpotify.m"; sourceTree = ""; }; 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = BGMApp.sdef; path = Scripting/BGMApp.sdef; sourceTree = ""; }; 1C2FC30D1EBC97DA00A76592 /* BGMApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMApp.h; path = BGMAppTests/UITests/BGMApp.h; sourceTree = SOURCE_ROOT; }; 1C2FC3121EC706E000A76592 /* BGMAppDelegate+AppleScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "BGMAppDelegate+AppleScript.h"; path = "Scripting/BGMAppDelegate+AppleScript.h"; sourceTree = ""; }; 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "BGMAppDelegate+AppleScript.mm"; path = "Scripting/BGMAppDelegate+AppleScript.mm"; sourceTree = ""; }; 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMASOutputDevice.mm; path = Scripting/BGMASOutputDevice.mm; sourceTree = ""; }; 1C2FC31D1EC723A100A76592 /* BGMASOutputDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMASOutputDevice.h; path = Scripting/BGMASOutputDevice.h; sourceTree = ""; }; 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMDeviceControlsList.cpp; sourceTree = ""; }; 1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlsList.h; sourceTree = ""; }; 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BGMAppVolumes.m; sourceTree = ""; }; 1C3DB48A1BE0888500EC8160 /* BGMAppVolumes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAppVolumes.h; sourceTree = ""; }; 1C43DABE22F582780004AF35 /* BGMApp.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = BGMApp.entitlements; sourceTree = ""; }; 1C4699461BD5C0E400F78043 /* BGMiTunes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMiTunes.m; path = "Music Players/BGMiTunes.m"; sourceTree = ""; }; 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMDeviceControlSync.cpp; sourceTree = ""; }; 1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlSync.h; sourceTree = ""; }; 1C4D1A1B217C7D6400A1ACD0 /* BGMPreferredOutputDevices.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMPreferredOutputDevices.h; sourceTree = ""; }; 1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMPreferredOutputDevices.mm; sourceTree = ""; }; 1C533C791EED28B700270802 /* uninstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = uninstall.sh; path = ../../uninstall.sh; sourceTree = ""; }; 1C533C7F1EF532CA00270802 /* _uninstall-non-interactive.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = "_uninstall-non-interactive.sh"; sourceTree = ""; }; 1C62FE4523D3EB2D00B9B68E /* MockAudioObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MockAudioObject.cpp; path = BGMAppTests/UnitTests/Mocks/MockAudioObject.cpp; sourceTree = SOURCE_ROOT; }; 1C62FE4623D3EB2D00B9B68E /* Mock_CAHALAudioObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioObject.cpp; path = BGMAppTests/UnitTests/Mocks/Mock_CAHALAudioObject.cpp; sourceTree = SOURCE_ROOT; }; 1C62FE4723D3EB2D00B9B68E /* MockAudioObjects.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MockAudioObjects.cpp; path = BGMAppTests/UnitTests/Mocks/MockAudioObjects.cpp; sourceTree = SOURCE_ROOT; }; 1C62FE4823D3EB2D00B9B68E /* Mock_CAHALAudioSystemObject.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioSystemObject.cpp; path = BGMAppTests/UnitTests/Mocks/Mock_CAHALAudioSystemObject.cpp; sourceTree = SOURCE_ROOT; }; 1C62FE4923D3EB2E00B9B68E /* MockAudioDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MockAudioDevice.h; path = BGMAppTests/UnitTests/Mocks/MockAudioDevice.h; sourceTree = SOURCE_ROOT; }; 1C62FE4A23D3EB2E00B9B68E /* Mock_CAHALAudioDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = Mock_CAHALAudioDevice.cpp; path = BGMAppTests/UnitTests/Mocks/Mock_CAHALAudioDevice.cpp; sourceTree = SOURCE_ROOT; }; 1C62FE4B23D3EB2E00B9B68E /* MockAudioDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = MockAudioDevice.cpp; path = BGMAppTests/UnitTests/Mocks/MockAudioDevice.cpp; sourceTree = SOURCE_ROOT; }; 1C62FE4C23D3EB2E00B9B68E /* MockAudioObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MockAudioObject.h; path = BGMAppTests/UnitTests/Mocks/MockAudioObject.h; sourceTree = SOURCE_ROOT; }; 1C62FE4D23D3EB2E00B9B68E /* MockAudioObjects.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MockAudioObjects.h; path = BGMAppTests/UnitTests/Mocks/MockAudioObjects.h; sourceTree = SOURCE_ROOT; }; 1C62FE5423D423D700B9B68E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/MacOSX.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 1C62FE5623D4278300B9B68E /* skip-ui-tests.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; name = "skip-ui-tests.py"; path = "UITests/skip-ui-tests.py"; sourceTree = ""; }; 1C62FE5923D44FC000B9B68E /* BGMApp-Debug.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "BGMApp-Debug.entitlements"; sourceTree = ""; }; 1C687A6A23B889E000834B75 /* BGMPlayThroughRTLoggerTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMPlayThroughRTLoggerTests.mm; path = UnitTests/BGMPlayThroughRTLoggerTests.mm; sourceTree = ""; }; 1C780FF01FEF6C3B00497FAD /* BGMSystemSoundsVolume.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMSystemSoundsVolume.h; sourceTree = ""; }; 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMSystemSoundsVolume.mm; sourceTree = ""; }; 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = ""; }; 1C8034C31BDAFD5700668E00 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = ""; }; 1C8034D420B0347A004BC50C /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 1C80DED120A6718600045BBE /* BGMAppWatcher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppWatcher.h; sourceTree = ""; }; 1C80DED220A6718600045BBE /* BGMAppWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BGMAppWatcher.m; sourceTree = ""; }; 1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputVolumeMenuItem.h; sourceTree = ""; }; 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputVolumeMenuItem.mm; sourceTree = ""; }; 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 = ""; }; 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMSwinsian.m; path = "Music Players/BGMSwinsian.m"; sourceTree = ""; }; 1C8D8303204238DB00A838F2 /* BGMSwinsian.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMSwinsian.h; path = "Music Players/BGMSwinsian.h"; sourceTree = ""; }; 1C8D83092042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMGooglePlayMusicDesktopPlayer.h; path = "Music Players/BGMGooglePlayMusicDesktopPlayer.h"; sourceTree = ""; }; 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMGooglePlayMusicDesktopPlayer.m; path = "Music Players/BGMGooglePlayMusicDesktopPlayer.m"; sourceTree = ""; }; 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = GooglePlayMusicDesktopPlayer.js; path = "Music Players/GooglePlayMusicDesktopPlayer.js"; sourceTree = ""; }; 1C9258452090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMGooglePlayMusicDesktopPlayerConnection.h; path = "Music Players/BGMGooglePlayMusicDesktopPlayerConnection.h"; sourceTree = ""; }; 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BGMGooglePlayMusicDesktopPlayerConnection.m; path = "Music Players/BGMGooglePlayMusicDesktopPlayerConnection.m"; sourceTree = ""; }; 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMBackgroundMusicDevice.cpp; sourceTree = ""; }; 1CACCF381F3175AD007F86CA /* BGMBackgroundMusicDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMBackgroundMusicDevice.h; sourceTree = ""; }; 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 = ""; }; 1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppDelegate.h; sourceTree = ""; }; 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppDelegate.mm; sourceTree = ""; }; 1CB8B33E1BBA75EF000E2DD1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 1CB8B3431BBA75EF000E2DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFArray.cpp; path = PublicUtility/CACFArray.cpp; sourceTree = ""; }; 1CC1DF7E1BE5068A00FB8FE4 /* CACFArray.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFArray.h; path = PublicUtility/CACFArray.h; sourceTree = ""; }; 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFDictionary.cpp; path = PublicUtility/CACFDictionary.cpp; sourceTree = ""; }; 1CC1DF801BE5068A00FB8FE4 /* CACFDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFDictionary.h; path = PublicUtility/CACFDictionary.h; sourceTree = ""; }; 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CADebugger.cpp; path = PublicUtility/CADebugger.cpp; sourceTree = ""; }; 1CC1DF901BE5891300FB8FE4 /* CADebugger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CADebugger.h; path = PublicUtility/CADebugger.h; sourceTree = ""; }; 1CC1DF951BE8607700FB8FE4 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMTermination.mm; sourceTree = ""; }; 1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMTermination.h; sourceTree = ""; }; 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 = ""; }; 1CCC4F4B1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMMusicPlayersUnitTests.mm; path = UnitTests/BGMMusicPlayersUnitTests.mm; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppVolumesController.mm; sourceTree = ""; }; 1CDE224022CBB95B0008E3AC /* Music.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Music.h; path = "Music Players/Music.h"; sourceTree = ""; }; 1CE03A55239B56740036908D /* BGMDebugLoggingMenuItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMDebugLoggingMenuItem.h; sourceTree = ""; }; 1CE03A56239B56740036908D /* BGMDebugLoggingMenuItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BGMDebugLoggingMenuItem.m; sourceTree = ""; }; 1CE7064A1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMOutputDeviceMenuSection.h; sourceTree = ""; }; 1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMOutputDeviceMenuSection.mm; sourceTree = ""; }; 1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDeviceManager.h; sourceTree = ""; }; 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAudioDeviceManager.mm; sourceTree = ""; }; 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMAudioDevice.cpp; sourceTree = ""; }; 1CF5423B1EAAEE4300445AD8 /* BGMAudioDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDevice.h; sourceTree = ""; }; 1CF69BA41BCFF59C009B5D1F /* BGMApp.profdata */ = {isa = PBXFileReference; lastKnownFileType = file; path = BGMApp.profdata; sourceTree = ""; }; 270A84501E0044EE00F13C99 /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; }; 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFNumber.cpp; path = PublicUtility/CACFNumber.cpp; sourceTree = ""; }; 271677B91C6CBDFA0080B0A2 /* CACFNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFNumber.h; path = PublicUtility/CACFNumber.h; sourceTree = ""; }; 27379B851C7C54870084A24C /* iTunes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = iTunes.h; path = "Music Players/iTunes.h"; sourceTree = ""; }; 27379B861C7C54870084A24C /* Spotify.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Spotify.h; path = "Music Players/Spotify.h"; sourceTree = ""; }; 27379B871C7C552A0084A24C /* VLC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VLC.h; path = "Music Players/VLC.h"; sourceTree = ""; }; 27379B881C7C562D0084A24C /* BGMVLC.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMVLC.h; path = "Music Players/BGMVLC.h"; sourceTree = ""; }; 27379B891C7C562D0084A24C /* BGMVLC.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMVLC.m; path = "Music Players/BGMVLC.m"; sourceTree = ""; }; 27379B8F1C7F57DA0084A24C /* BGMXPCHelper.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = BGMXPCHelper.xpc; sourceTree = BUILT_PRODUCTS_DIR; }; 273F10DC1CC3CF9C00C1C6DA /* VOX.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VOX.h; path = "Music Players/VOX.h"; sourceTree = ""; }; 273F10DD1CC3D0B900C1C6DA /* BGMVOX.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMVOX.h; path = "Music Players/BGMVOX.h"; sourceTree = ""; }; 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMVOX.m; path = "Music Players/BGMVOX.m"; sourceTree = ""; }; 2743C9E71D852B350089613B /* BGMMusicPlayers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMMusicPlayers.h; path = "Music Players/BGMMusicPlayers.h"; sourceTree = ""; }; 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMMusicPlayers.mm; path = "Music Players/BGMMusicPlayers.mm"; sourceTree = ""; }; 2743C9E91D852B350089613B /* BGMScriptingBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMScriptingBridge.h; path = "Music Players/BGMScriptingBridge.h"; sourceTree = ""; }; 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMScriptingBridge.m; path = "Music Players/BGMScriptingBridge.m"; sourceTree = ""; }; 2743C9ED1D8538700089613B /* BGMUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMUserDefaults.h; sourceTree = ""; }; 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BGMUserDefaults.m; sourceTree = ""; }; 2743C9F61D86CFF90089613B /* BGMAppUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMAppUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2743CA1C1D86DA9B0089613B /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 275343BF1DFD01BC00DF3858 /* SystemPreferences.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SystemPreferences.h; sourceTree = ""; }; 2769728B1CAFCEE8007A2F7C /* post_install.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = post_install.sh; path = BGMXPCHelper/post_install.sh; sourceTree = SOURCE_ROOT; }; 2769728D1CAFCEFD007A2F7C /* com.bearisdriving.BGM.XPCHelper.plist.template */ = {isa = PBXFileReference; explicitFileType = text.xml; fileEncoding = 4; name = com.bearisdriving.BGM.XPCHelper.plist.template; path = BGMXPCHelper/com.bearisdriving.BGM.XPCHelper.plist.template; sourceTree = SOURCE_ROOT; }; 276972901CB16008007A2F7C /* safe_install_dir.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; name = safe_install_dir.sh; path = BGMXPCHelper/safe_install_dir.sh; sourceTree = SOURCE_ROOT; }; 2771700F1CA0C83B00AB34B4 /* BGM_Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGM_Utils.h; path = ../SharedSource/BGM_Utils.h; sourceTree = ""; }; 277170141CA24D7C00AB34B4 /* BGMXPCListenerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMXPCListenerDelegate.h; path = BGMXPCHelper/BGMXPCListenerDelegate.h; sourceTree = SOURCE_ROOT; }; 277170151CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMXPCListenerDelegate.m; path = BGMXPCHelper/BGMXPCListenerDelegate.m; sourceTree = SOURCE_ROOT; }; 278D71F11CABB6FF00899CF9 /* BGMXPCHelperTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMXPCHelperTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2795970D1C91589B00A002FB /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMXPCListener.mm; sourceTree = ""; }; 2795973C1C982E8C00A002FB /* BGMXPCListener.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMXPCListener.h; sourceTree = ""; }; 279F48751DD6D73900768A85 /* BGMHermes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMHermes.h; path = "Music Players/BGMHermes.h"; sourceTree = ""; }; 279F48761DD6D73900768A85 /* BGMHermes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMHermes.m; path = "Music Players/BGMHermes.m"; sourceTree = ""; }; 279F48781DD6D94000768A85 /* Hermes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Hermes.h; path = "Music Players/Hermes.h"; sourceTree = ""; }; 27C457E41CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAutoPauseMenuItem.h; sourceTree = ""; }; 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BGMAutoPauseMenuItem.m; sourceTree = ""; }; 27D1D6B91DD7226C0049E707 /* BGMAboutPanel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMAboutPanel.h; path = Preferences/BGMAboutPanel.h; sourceTree = ""; }; 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMAboutPanel.m; path = Preferences/BGMAboutPanel.m; sourceTree = ""; }; 27D643B41C9FABBD00737F6E /* BGM_Types.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGM_Types.h; path = ../SharedSource/BGM_Types.h; sourceTree = ""; }; 27D643B51C9FABBD00737F6E /* BGMXPCProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMXPCProtocols.h; path = ../SharedSource/BGMXPCProtocols.h; sourceTree = ""; }; 27D643B91C9FB84C00737F6E /* BGMXPCHelperService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMXPCHelperService.h; path = BGMXPCHelper/BGMXPCHelperService.h; 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 = ""; }; 27F7D48E1D2483B100821C4B /* BGMDecibel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMDecibel.h; path = "Music Players/BGMDecibel.h"; sourceTree = ""; }; 27F7D48F1D2483B100821C4B /* BGMDecibel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMDecibel.m; path = "Music Players/BGMDecibel.m"; sourceTree = ""; }; 27F7D4911D2484A300821C4B /* Decibel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Decibel.h; path = "Music Players/Decibel.h"; sourceTree = ""; }; 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = BGM_Utils.cpp; path = ../SharedSource/BGM_Utils.cpp; sourceTree = ""; }; 9E129A3F2602AE620005851B /* BGMASApplication.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMASApplication.h; path = Scripting/BGMASApplication.h; sourceTree = ""; }; 9E129A402602AE620005851B /* BGMASApplication.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BGMASApplication.m; path = Scripting/BGMASApplication.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 1CB8B3331BBA75EF000E2DD1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1C8B0C6A216205BF008C5679 /* AVFoundation.framework in Frameworks */, 1C8034D520B0347A004BC50C /* Security.framework in Frameworks */, 270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */, 1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */, 1C1963031BCAC160008A4DF7 /* CoreAudio.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 1CCC4F511E584081008053E4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1C62FE5523D423D700B9B68E /* XCTest.framework in Frameworks */, 1C8B0C6B21645355008C5679 /* AVFoundation.framework in Frameworks */, 1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 27379B8C1C7F57DA0084A24C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1C1AA4B31F9DE40000BCFB22 /* AudioToolbox.framework in Frameworks */, 1CF2D58F1F944773008B6E35 /* CoreAudio.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 2743C9F31D86CFF90089613B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 2743CA231D86DEA70089613B /* AudioToolbox.framework in Frameworks */, 2743CA221D86DE960089613B /* CoreAudio.framework in Frameworks */, 2743CA1D1D86DA9B0089613B /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 278D71EB1CABB6FF00899CF9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 1C09150623F010FB001EB0E1 /* Scripts */ = { isa = PBXGroup; children = ( 1C09150723F010FB001EB0E1 /* set-version.sh */, ); name = Scripts; path = ../SharedSource/Scripts; sourceTree = ""; }; 1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */ = { isa = PBXGroup; children = ( 1C0BD0A61BF1B029004F4CF5 /* BGMPreferencesMenu.h */, 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */, 27D1D6B91DD7226C0049E707 /* BGMAboutPanel.h */, 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */, 1C0BD0A31BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.h */, 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */, ); name = "Preferences Menu"; sourceTree = ""; }; 1C1962E11BC94DDF008A4DF7 /* PublicUtility */ = { isa = PBXGroup; children = ( 1C1962E81BC95301008A4DF7 /* CAAtomic.h */, 1C1962E91BC95301008A4DF7 /* CAAutoDisposer.h */, 1C1962EA1BC95301008A4DF7 /* CABitOperations.h */, 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */, 1CC1DF7E1BE5068A00FB8FE4 /* CACFArray.h */, 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */, 1CC1DF801BE5068A00FB8FE4 /* CACFDictionary.h */, 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */, 271677B91C6CBDFA0080B0A2 /* CACFNumber.h */, 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */, 1C1963001BCAC0F6008A4DF7 /* CACFString.h */, 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */, 1CC1DF901BE5891300FB8FE4 /* CADebugger.h */, 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */, 1C1962F81BCAC061008A4DF7 /* CADebugMacros.h */, 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */, 1C1962FC1BCAC0C3008A4DF7 /* CADebugPrintf.h */, 1C1962FE1BCAC0DB008A4DF7 /* CAException.h */, 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */, 1C1962EC1BCABFC5008A4DF7 /* CAHALAudioDevice.h */, 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */, 1C1962EE1BCABFC5008A4DF7 /* CAHALAudioObject.h */, 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */, 1C1962F01BCABFC5008A4DF7 /* CAHALAudioStream.h */, 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */, 1C1962F21BCABFC5008A4DF7 /* CAHALAudioSystemObject.h */, 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */, 1C1963081BCAF677008A4DF7 /* CAHostTimeBase.h */, 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */, 1C1963051BCAF468008A4DF7 /* CAMutex.h */, 1C1962F91BCAC061008A4DF7 /* CAPropertyAddress.h */, 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */, 1C8034C31BDAFD5700668E00 /* CAPThread.h */, 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */, 1C1962E31BC94E15008A4DF7 /* CARingBuffer.h */, 19FE7908A33FA7BD97B432D9 /* BGMDebugLogging.h */, 19FE73389459BF65748F531F /* BGMDebugLogging.c */, 19FE71BCD79E7246F7345C16 /* BGMThreadSafetyAnalysis.h */, ); name = PublicUtility; sourceTree = ""; }; 1C2FC3161EC7078F00A76592 /* Scripting */ = { isa = PBXGroup; children = ( 1C2FC3121EC706E000A76592 /* BGMAppDelegate+AppleScript.h */, 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */, 1C2FC31D1EC723A100A76592 /* BGMASOutputDevice.h */, 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */, 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */, 9E129A3F2602AE620005851B /* BGMASApplication.h */, 9E129A402602AE620005851B /* BGMASApplication.m */, ); name = Scripting; sourceTree = ""; }; 1C4699401BD5BA1700F78043 /* SharedSource */ = { isa = PBXGroup; children = ( 27D643B41C9FABBD00737F6E /* BGM_Types.h */, 1C09150623F010FB001EB0E1 /* Scripts */, 2771700F1CA0C83B00AB34B4 /* BGM_Utils.h */, 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */, 27D643C41C9FBE5600737F6E /* BGM_TestUtils.h */, 27D643B51C9FABBD00737F6E /* BGMXPCProtocols.h */, ); name = SharedSource; sourceTree = ""; }; 1C4699451BD5BF2E00F78043 /* Music Players */ = { isa = PBXGroup; children = ( 2743C9E71D852B350089613B /* BGMMusicPlayers.h */, 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */, 2743C9E91D852B350089613B /* BGMScriptingBridge.h */, 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */, 1C2336DC1BEAB73F004C1C4E /* BGMMusicPlayer.h */, 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */, 27F7D48E1D2483B100821C4B /* BGMDecibel.h */, 27F7D48F1D2483B100821C4B /* BGMDecibel.m */, 1C8D83092042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.h */, 1C8D830A2042DE9500A838F2 /* BGMGooglePlayMusicDesktopPlayer.m */, 1C9258452090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.h */, 1C9258462090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m */, 1C2336DB1BEAB73F004C1C4E /* BGMiTunes.h */, 1C4699461BD5C0E400F78043 /* BGMiTunes.m */, 19FE70CF6C93F5007940CE91 /* BGMMusic.h */, 19FE73822ADD50BA9120AB05 /* BGMMusic.m */, 1C2336DD1BEAE10C004C1C4E /* BGMSpotify.h */, 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */, 279F48751DD6D73900768A85 /* BGMHermes.h */, 279F48761DD6D73900768A85 /* BGMHermes.m */, 1C8D8303204238DB00A838F2 /* BGMSwinsian.h */, 1C8D8302204238DB00A838F2 /* BGMSwinsian.m */, 27379B881C7C562D0084A24C /* BGMVLC.h */, 27379B891C7C562D0084A24C /* BGMVLC.m */, 273F10DD1CC3D0B900C1C6DA /* BGMVOX.h */, 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */, 27379B841C7C53BE0084A24C /* Supporting Files */, ); name = "Music Players"; sourceTree = ""; }; 1C62FE4423D3EAC500B9B68E /* Mocks */ = { isa = PBXGroup; children = ( 1C62FE4823D3EB2D00B9B68E /* Mock_CAHALAudioSystemObject.cpp */, 1C62FE4623D3EB2D00B9B68E /* Mock_CAHALAudioObject.cpp */, 1C62FE4A23D3EB2E00B9B68E /* Mock_CAHALAudioDevice.cpp */, 1C62FE4D23D3EB2E00B9B68E /* MockAudioObjects.h */, 1C62FE4723D3EB2D00B9B68E /* MockAudioObjects.cpp */, 1C62FE4C23D3EB2E00B9B68E /* MockAudioObject.h */, 1C62FE4523D3EB2D00B9B68E /* MockAudioObject.cpp */, 1C62FE4923D3EB2E00B9B68E /* MockAudioDevice.h */, 1C62FE4B23D3EB2E00B9B68E /* MockAudioDevice.cpp */, ); path = Mocks; sourceTree = ""; }; 1CB8B32D1BBA75EF000E2DD1 = { isa = PBXGroup; children = ( 1CB8B3381BBA75EF000E2DD1 /* BGMApp */, 1CB8B34C1BBA75F0000E2DD1 /* BGMApp Tests */, 27379B901C7F57DB0084A24C /* BGMXPCHelper */, 1CCC4F3A1E581691008053E4 /* BGMXPCHelper Tests */, 1C4699401BD5BA1700F78043 /* SharedSource */, 1C1962E11BC94DDF008A4DF7 /* PublicUtility */, 1CB8B3371BBA75EF000E2DD1 /* Products */, 1CF69BA31BCFF59C009B5D1F /* OptimizationProfiles */, 2743CA1B1D86DA9B0089613B /* Frameworks */, ); sourceTree = ""; }; 1CB8B3371BBA75EF000E2DD1 /* Products */ = { isa = PBXGroup; children = ( 1CB8B3361BBA75EF000E2DD1 /* Background Music.app */, 27379B8F1C7F57DA0084A24C /* BGMXPCHelper.xpc */, 278D71F11CABB6FF00899CF9 /* BGMXPCHelperTests.xctest */, 2743C9F61D86CFF90089613B /* BGMAppUnitTests.xctest */, 1CCC4F541E584081008053E4 /* BGMAppUITests.xctest */, ); name = Products; sourceTree = ""; }; 1CB8B3381BBA75EF000E2DD1 /* BGMApp */ = { isa = PBXGroup; children = ( 1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */, 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */, 1C80DED120A6718600045BBE /* BGMAppWatcher.h */, 1C80DED220A6718600045BBE /* BGMAppWatcher.m */, 1C837DD61F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.h */, 1C837DD71F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm */, 1CE03A55239B56740036908D /* BGMDebugLoggingMenuItem.h */, 1CE03A56239B56740036908D /* BGMDebugLoggingMenuItem.m */, 1C780FF01FEF6C3B00497FAD /* BGMSystemSoundsVolume.h */, 1C780FF11FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm */, 1C3DB48A1BE0888500EC8160 /* BGMAppVolumes.h */, 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.m */, 1CD410D21F9EDDAD0070A094 /* BGMAppVolumesController.h */, 1CD410D31F9EDDAD0070A094 /* BGMAppVolumesController.mm */, 1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */, 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */, 1C4D1A1B217C7D6400A1ACD0 /* BGMPreferredOutputDevices.h */, 1C4D1A1C217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm */, 1CF5423B1EAAEE4300445AD8 /* BGMAudioDevice.h */, 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */, 1CACCF381F3175AD007F86CA /* BGMBackgroundMusicDevice.h */, 1CACCF371F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp */, 27C457E41CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.h */, 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */, 1C1465B91BCC49D1003AEFE6 /* BGMAutoPauseMusic.h */, 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */, 1C4699451BD5BF2E00F78043 /* Music Players */, 1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */, 1CE7064A1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.h */, 1CE7064B1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm */, 1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */, 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */, 1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */, 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */, 1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */, 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */, 19FE72A176FD500FB4C1F5C6 /* BGMPlayThroughRTLogger.h */, 19FE7DE5E3BA0046ED2BC3C6 /* BGMPlayThroughRTLogger.cpp */, 19FE799A86A285DD9423D164 /* BGMStatusBarItem.h */, 19FE774DD758EC163EF4F28C /* BGMStatusBarItem.mm */, 1CC6593B1F91DEB400B0CCDC /* BGMTermination.h */, 1CC6593A1F91DEB400B0CCDC /* BGMTermination.mm */, 2743C9ED1D8538700089613B /* BGMUserDefaults.h */, 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */, 19FE7FDAEBC3F0DB8C99823B /* BGMVolumeChangeListener.h */, 19FE7179EBFA116F3861E79D /* BGMVolumeChangeListener.cpp */, 2795973C1C982E8C00A002FB /* BGMXPCListener.h */, 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */, 1C2FC3161EC7078F00A76592 /* Scripting */, 1CB8B3421BBA75EF000E2DD1 /* MainMenu.xib */, 1CB8B3391BBA75EF000E2DD1 /* Supporting Files */, ); path = BGMApp; sourceTree = ""; }; 1CB8B3391BBA75EF000E2DD1 /* Supporting Files */ = { isa = PBXGroup; children = ( 1C43DABE22F582780004AF35 /* BGMApp.entitlements */, 1C62FE5923D44FC000B9B68E /* BGMApp-Debug.entitlements */, 275343BF1DFD01BC00DF3858 /* SystemPreferences.h */, 1CED61681C3081C2002CAFCF /* LICENSE */, 1CC1DF951BE8607700FB8FE4 /* Images.xcassets */, 1CB8B33A1BBA75EF000E2DD1 /* Info.plist */, 1C533C7F1EF532CA00270802 /* _uninstall-non-interactive.sh */, 1C533C791EED28B700270802 /* uninstall.sh */, 1CB8B33E1BBA75EF000E2DD1 /* main.m */, ); name = "Supporting Files"; sourceTree = ""; }; 1CB8B34C1BBA75F0000E2DD1 /* BGMApp Tests */ = { isa = PBXGroup; children = ( 1CCC4F551E584081008053E4 /* UI Tests */, 1CCC4F4F1E581C52008053E4 /* Unit Tests */, 1CCC4F481E581BAA008053E4 /* Supporting Files */, ); name = "BGMApp Tests"; path = BGMAppTests; sourceTree = ""; }; 1CCC4F3A1E581691008053E4 /* BGMXPCHelper Tests */ = { isa = PBXGroup; children = ( 1CCC4F3C1E58196C008053E4 /* BGMXPCHelperTests.m */, 1CCC4F3F1E58198C008053E4 /* Supporting Files */, ); name = "BGMXPCHelper Tests"; path = BGMService; sourceTree = ""; }; 1CCC4F3F1E58198C008053E4 /* Supporting Files */ = { isa = PBXGroup; children = ( 1CCC4F3B1E58196C008053E4 /* BGMXPCHelperTests-Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; 1CCC4F481E581BAA008053E4 /* Supporting Files */ = { isa = PBXGroup; children = ( 1CCC4F5F1E5840EF008053E4 /* BGMAppUITests-Info.plist */, 1CCC4F491E581C0D008053E4 /* BGMAppUnitTests-Info.plist */, 1C62FE5623D4278300B9B68E /* skip-ui-tests.py */, ); name = "Supporting Files"; sourceTree = ""; }; 1CCC4F4F1E581C52008053E4 /* Unit Tests */ = { isa = PBXGroup; children = ( 1CCC4F4B1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm */, 19FE761D0371DEF9FDF053D6 /* BGMPlayThroughTests.mm */, 1C687A6A23B889E000834B75 /* BGMPlayThroughRTLoggerTests.mm */, 1C62FE4423D3EAC500B9B68E /* Mocks */, ); name = "Unit Tests"; sourceTree = ""; }; 1CCC4F551E584081008053E4 /* UI Tests */ = { isa = PBXGroup; children = ( 1C2FC30D1EBC97DA00A76592 /* BGMApp.h */, 1CCC4F611E584100008053E4 /* BGMAppUITests.mm */, ); name = "UI Tests"; path = ../BGMAppUITests; sourceTree = ""; }; 1CF69BA31BCFF59C009B5D1F /* OptimizationProfiles */ = { isa = PBXGroup; children = ( 1CF69BA41BCFF59C009B5D1F /* BGMApp.profdata */, ); path = OptimizationProfiles; sourceTree = ""; }; 27379B841C7C53BE0084A24C /* Supporting Files */ = { isa = PBXGroup; children = ( 27F7D4911D2484A300821C4B /* Decibel.h */, 279F48781DD6D94000768A85 /* Hermes.h */, 27379B851C7C54870084A24C /* iTunes.h */, 1CDE224022CBB95B0008E3AC /* Music.h */, 1C8D830D2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js */, 27379B861C7C54870084A24C /* Spotify.h */, 1C8D8301204238DB00A838F2 /* Swinsian.h */, 27379B871C7C552A0084A24C /* VLC.h */, 273F10DC1CC3CF9C00C1C6DA /* VOX.h */, ); name = "Supporting Files"; sourceTree = ""; }; 27379B901C7F57DB0084A24C /* BGMXPCHelper */ = { isa = PBXGroup; children = ( 27D643B91C9FB84C00737F6E /* BGMXPCHelperService.h */, 27D643BA1C9FB84C00737F6E /* BGMXPCHelperService.mm */, 277170141CA24D7C00AB34B4 /* BGMXPCListenerDelegate.h */, 277170151CA24D7C00AB34B4 /* BGMXPCListenerDelegate.m */, 27D643BC1C9FB84C00737F6E /* main.m */, 2769728A1CAFCEA9007A2F7C /* Supporting Files */, ); name = BGMXPCHelper; path = BGMService; sourceTree = ""; }; 2743CA1B1D86DA9B0089613B /* Frameworks */ = { isa = PBXGroup; children = ( 1C62FE5423D423D700B9B68E /* XCTest.framework */, 1C8034D420B0347A004BC50C /* Security.framework */, 1C8B0C69216205BF008C5679 /* AVFoundation.framework */, 270A84501E0044EE00F13C99 /* ScriptingBridge.framework */, 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */, 1C1963021BCAC160008A4DF7 /* CoreAudio.framework */, 2795970D1C91589B00A002FB /* CoreFoundation.framework */, 2743CA1C1D86DA9B0089613B /* Foundation.framework */, ); name = Frameworks; sourceTree = ""; }; 2769728A1CAFCEA9007A2F7C /* Supporting Files */ = { isa = PBXGroup; children = ( 2769728D1CAFCEFD007A2F7C /* com.bearisdriving.BGM.XPCHelper.plist.template */, 2769728B1CAFCEE8007A2F7C /* post_install.sh */, 276972901CB16008007A2F7C /* safe_install_dir.sh */, 27D643BB1C9FB84C00737F6E /* Info.plist */, ); name = "Supporting Files"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 1CB8B3351BBA75EF000E2DD1 /* Background Music */ = { isa = PBXNativeTarget; buildConfigurationList = 1CB8B3531BBA75F0000E2DD1 /* Build configuration list for PBXNativeTarget "Background Music" */; buildPhases = ( 1CB8B3321BBA75EF000E2DD1 /* Sources */, 1CB8B3331BBA75EF000E2DD1 /* Frameworks */, 1CB8B3341BBA75EF000E2DD1 /* Resources */, 1CD440581E593DDD0064E0BC /* Run Script - set-version.sh */, ); buildRules = ( ); dependencies = ( ); name = "Background Music"; productName = BGMApp; productReference = 1CB8B3361BBA75EF000E2DD1 /* Background Music.app */; productType = "com.apple.product-type.application"; }; 1CCC4F531E584081008053E4 /* BGMAppUITests */ = { isa = PBXNativeTarget; buildConfigurationList = 1CCC4F5B1E584081008053E4 /* Build configuration list for PBXNativeTarget "BGMAppUITests" */; buildPhases = ( 1CCC4F501E584081008053E4 /* Sources */, 1CCC4F511E584081008053E4 /* Frameworks */, 1CCC4F521E584081008053E4 /* Resources */, ); buildRules = ( ); dependencies = ( 1CCC4F5A1E584081008053E4 /* PBXTargetDependency */, ); name = BGMAppUITests; productName = BGMAppUITests; productReference = 1CCC4F541E584081008053E4 /* BGMAppUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 27379B8E1C7F57DA0084A24C /* BGMXPCHelper */ = { isa = PBXNativeTarget; buildConfigurationList = 27379B981C7F57DB0084A24C /* Build configuration list for PBXNativeTarget "BGMXPCHelper" */; buildPhases = ( 27379B8B1C7F57DA0084A24C /* Sources */, 27379B8C1C7F57DA0084A24C /* Frameworks */, 27379B8D1C7F57DA0084A24C /* Resources */, 1C09150923F0208F001EB0E1 /* Run Script - set-version.sh */, 276972891CAFCE91007A2F7C /* Run Script - post_install.sh */, ); buildRules = ( ); dependencies = ( ); name = BGMXPCHelper; productName = BGMService; productReference = 27379B8F1C7F57DA0084A24C /* BGMXPCHelper.xpc */; productType = "com.apple.product-type.xpc-service"; }; 2743C9F51D86CFF90089613B /* BGMAppUnitTests */ = { isa = PBXNativeTarget; buildConfigurationList = 2743C9FD1D86CFFA0089613B /* Build configuration list for PBXNativeTarget "BGMAppUnitTests" */; buildPhases = ( 2743C9F21D86CFF90089613B /* Sources */, 2743C9F31D86CFF90089613B /* Frameworks */, 2743C9F41D86CFF90089613B /* Resources */, ); buildRules = ( ); dependencies = ( ); name = BGMAppUnitTests; productName = AppUnitTests; productReference = 2743C9F61D86CFF90089613B /* BGMAppUnitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 278D71E51CABB6FF00899CF9 /* BGMXPCHelperTests */ = { isa = PBXNativeTarget; buildConfigurationList = 278D71ED1CABB6FF00899CF9 /* Build configuration list for PBXNativeTarget "BGMXPCHelperTests" */; buildPhases = ( 278D71E81CABB6FF00899CF9 /* Sources */, 278D71EB1CABB6FF00899CF9 /* Frameworks */, 278D71EC1CABB6FF00899CF9 /* Resources */, ); buildRules = ( ); dependencies = ( 278D71F41CABB88B00899CF9 /* PBXTargetDependency */, ); name = BGMXPCHelperTests; productName = BGMAppTests; productReference = 278D71F11CABB6FF00899CF9 /* BGMXPCHelperTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 1CB8B32E1BBA75EF000E2DD1 /* Project object */ = { isa = PBXProject; attributes = { CLASSPREFIX = BGM; KnownAssetTags = ( New, ); LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Background Music contributors"; TargetAttributes = { 1CB8B3351BBA75EF000E2DD1 = { CreatedOnToolsVersion = 6.4; SystemCapabilities = { com.apple.Sandbox = { enabled = 0; }; }; }; 1CCC4F531E584081008053E4 = { CreatedOnToolsVersion = 8.2.1; ProvisioningStyle = Manual; TestTargetID = 1CB8B3351BBA75EF000E2DD1; }; 27379B8E1C7F57DA0084A24C = { CreatedOnToolsVersion = 7.2.1; DevelopmentTeam = PR7PXC66S5; }; 2743C9F51D86CFF90089613B = { CreatedOnToolsVersion = 8.0; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 1CB8B3311BBA75EF000E2DD1 /* Build configuration list for PBXProject "BGMApp" */; compatibilityVersion = "Xcode 6.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 1CB8B32D1BBA75EF000E2DD1; productRefGroup = 1CB8B3371BBA75EF000E2DD1 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 1CB8B3351BBA75EF000E2DD1 /* Background Music */, 1CCC4F531E584081008053E4 /* BGMAppUITests */, 2743C9F51D86CFF90089613B /* BGMAppUnitTests */, 27379B8E1C7F57DA0084A24C /* BGMXPCHelper */, 278D71E51CABB6FF00899CF9 /* BGMXPCHelperTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 1CB8B3341BBA75EF000E2DD1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 274827951E11052500B31D8D /* MainMenu.xib in Resources */, 1C533C7A1EED28B700270802 /* uninstall.sh in Resources */, 1C8D830E2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */, 1C533C801EF532CA00270802 /* _uninstall-non-interactive.sh in Resources */, 1CED61691C3081C2002CAFCF /* LICENSE in Resources */, 1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */, 1CC1DF961BE8607700FB8FE4 /* Images.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 1CCC4F521E584081008053E4 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 1C62FE5823D4278300B9B68E /* skip-ui-tests.py in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 27379B8D1C7F57DA0084A24C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 1C533C7C1EED2F8A00270802 /* com.bearisdriving.BGM.XPCHelper.plist.template in Resources */, 1C533C7B1EED2F6200270802 /* safe_install_dir.sh in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2743C9F41D86CFF90089613B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 1C8D830F2042F25C00A838F2 /* GooglePlayMusicDesktopPlayer.js in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 278D71EC1CABB6FF00899CF9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 1C09150923F0208F001EB0E1 /* Run Script - set-version.sh */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); name = "Run Script - set-version.sh"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Append the git HEAD short ID to the build version for SNAPSHOT and DEBUG builds.\n\"$SRCROOT/../SharedSource/Scripts/set-version.sh\"\n"; }; 1CD440581E593DDD0064E0BC /* Run Script - set-version.sh */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script - set-version.sh"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "# Append the git HEAD short ID to the build version for SNAPSHOT and DEBUG builds.\n\"$SRCROOT/../SharedSource/Scripts/set-version.sh\"\n"; }; 276972891CAFCE91007A2F7C /* Run Script - post_install.sh */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 8; files = ( ); inputPaths = ( ); name = "Run Script - post_install.sh"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/bash; shellScript = "/bin/bash \"$SRCROOT/BGMXPCHelper/post_install.sh\"\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 1CB8B3321BBA75EF000E2DD1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 1C86DA6A1F91EE3B000C8CCF /* CAPThread.cpp in Sources */, 1C780FF21FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */, 1CE03A57239B56740036908D /* BGMDebugLoggingMenuItem.m in Sources */, 1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */, 1CD410D41F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */, 1C1962E41BC94E15008A4DF7 /* CARingBuffer.cpp in Sources */, 1C8D830B2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */, 273F10DF1CC3D0B900C1C6DA /* BGMVOX.m in Sources */, 1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */, 279F48771DD6D73A00768A85 /* BGMHermes.m in Sources */, 1C0BD0A81BF1B029004F4CF5 /* BGMPreferencesMenu.mm in Sources */, 1C1962F41BCABFC5008A4DF7 /* CAHALAudioObject.cpp in Sources */, 27F7D4901D2483B100821C4B /* BGMDecibel.m in Sources */, 1C4D1A1D217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */, 1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */, 1C1962F61BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp in Sources */, 1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */, 1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.m in Sources */, 1C0BD0A51BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm in Sources */, 1C1963061BCAF468008A4DF7 /* CAMutex.cpp in Sources */, 1CE7064C1BF1EC0600BFC06D /* BGMOutputDeviceMenuSection.mm in Sources */, 1CACCF391F3175AD007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */, 1C1962F51BCABFC5008A4DF7 /* CAHALAudioStream.cpp in Sources */, 1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */, 2743C9EB1D852B360089613B /* BGMMusicPlayers.mm in Sources */, 1C1963091BCAF677008A4DF7 /* CAHostTimeBase.cpp in Sources */, 1C2FC3141EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */, 1C2FC31B1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */, 1CB8B33F1BBA75EF000E2DD1 /* main.m in Sources */, 1CB8B33D1BBA75EF000E2DD1 /* BGMAppDelegate.mm in Sources */, 271677BA1C6CBDFA0080B0A2 /* CACFNumber.cpp in Sources */, 27D1D6BB1DD7226C0049E707 /* BGMAboutPanel.m in Sources */, 27379B8A1C7C562D0084A24C /* BGMVLC.m in Sources */, 1C2336DF1BEAE10C004C1C4E /* BGMSpotify.m in Sources */, 1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */, 2743C9F11D853FBB0089613B /* BGMUserDefaults.m in Sources */, 1C1962FD1BCAC0C3008A4DF7 /* CADebugPrintf.cpp in Sources */, 1C80DED320A6718600045BBE /* BGMAppWatcher.m in Sources */, 2743C9EC1D852B360089613B /* BGMScriptingBridge.m in Sources */, 1CC6593C1F91DEB400B0CCDC /* BGMTermination.mm in Sources */, 1C837DD81F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */, 1C9258472090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */, 1C1963011BCAC0F6008A4DF7 /* CACFString.cpp in Sources */, 9E129A412602AE620005851B /* BGMASApplication.m 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 */, 1CF5423C1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */, 1CC1DF911BE5891300FB8FE4 /* CADebugger.cpp in Sources */, 1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */, 2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */, 27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */, 1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */, 19FE7F77376562C179449013 /* BGMStatusBarItem.mm in Sources */, 19FE719951725A698A419CBA /* BGMVolumeChangeListener.cpp in Sources */, 19FE72566BCEB11BD1F3D487 /* BGMMusic.m in Sources */, 19FE70F73D26D54450779A22 /* BGMPlayThroughRTLogger.cpp in Sources */, 19FE7B7BDF0C683288654F90 /* BGMDebugLogging.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 1CCC4F501E584081008053E4 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 1C8104B022AD082E00B35517 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */, 1C8D830520423E1C00A838F2 /* BGMSwinsian.m in Sources */, 1CE03A58239B56740036908D /* BGMDebugLoggingMenuItem.m in Sources */, 1CACCF3A1F334447007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */, 1C780FF31FEF6C3B00497FAD /* BGMSystemSoundsVolume.mm in Sources */, 1CC6593D1F91DEB400B0CCDC /* BGMTermination.mm in Sources */, 1C80DED420A6718600045BBE /* BGMAppWatcher.m in Sources */, 1CD410D51F9EDDAD0070A094 /* BGMAppVolumesController.mm in Sources */, 1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */, 1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */, 1CD989591ECFFD250014BBBF /* CAPThread.cpp in Sources */, 1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */, 1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.m in Sources */, 1CD989431ECFFCFC0014BBBF /* BGMAudioDeviceManager.mm in Sources */, 1C4D1A1E217C7D6400A1ACD0 /* BGMPreferredOutputDevices.mm in Sources */, 1CD989441ECFFCFC0014BBBF /* BGMAutoPauseMenuItem.m in Sources */, 1CD989451ECFFCFC0014BBBF /* BGMAutoPauseMusic.mm in Sources */, 1C837DD91F6AA1F2004B1E60 /* BGMOutputVolumeMenuItem.mm in Sources */, 1CD989461ECFFCFC0014BBBF /* BGMMusicPlayers.mm in Sources */, 1CD989471ECFFCFC0014BBBF /* BGMScriptingBridge.m in Sources */, 1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */, 1CD989491ECFFCFC0014BBBF /* BGMDecibel.m in Sources */, 1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */, 1C9258482090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */, 1CD9894A1ECFFCFC0014BBBF /* BGMiTunes.m in Sources */, 1CD9894B1ECFFCFC0014BBBF /* BGMSpotify.m in Sources */, 1CD9894C1ECFFCFC0014BBBF /* BGMHermes.m in Sources */, 1CD9894D1ECFFCFC0014BBBF /* BGMVLC.m in Sources */, 1CD9894E1ECFFCFC0014BBBF /* BGMVOX.m in Sources */, 1CD9894F1ECFFCFC0014BBBF /* BGMPreferencesMenu.mm in Sources */, 1CD989501ECFFCFC0014BBBF /* BGMAboutPanel.m in Sources */, 1CD989511ECFFCFC0014BBBF /* BGMAutoPauseMusicPrefs.mm in Sources */, 1CD989521ECFFCFC0014BBBF /* BGMOutputDeviceMenuSection.mm in Sources */, 1CD989531ECFFCFC0014BBBF /* BGMDeviceControlSync.cpp in Sources */, 1CD989541ECFFCFC0014BBBF /* BGMPlayThrough.cpp in Sources */, 1CD989551ECFFCFC0014BBBF /* BGMUserDefaults.m in Sources */, 1CD989561ECFFCFC0014BBBF /* BGMXPCListener.mm in Sources */, 1CD989411ECFFCD10014BBBF /* BGMAppDelegate.mm in Sources */, 1CD989341ECFFC9E0014BBBF /* BGM_Utils.cpp in Sources */, 1CD989351ECFFC9E0014BBBF /* CACFArray.cpp in Sources */, 1CD989361ECFFC9E0014BBBF /* CACFDictionary.cpp in Sources */, 1CD989371ECFFC9E0014BBBF /* CACFNumber.cpp in Sources */, 1CD989381ECFFC9E0014BBBF /* CACFString.cpp in Sources */, 9E542C7026057FBA0016C0B5 /* BGMASApplication.m in Sources */, 1CD989391ECFFC9E0014BBBF /* CADebugger.cpp in Sources */, 1CD9893A1ECFFC9E0014BBBF /* CADebugMacros.cpp in Sources */, 1CD9893B1ECFFC9E0014BBBF /* CADebugPrintf.cpp in Sources */, 1CD9893C1ECFFC9E0014BBBF /* CAHALAudioDevice.cpp in Sources */, 1CD9893D1ECFFC9E0014BBBF /* CAHALAudioObject.cpp in Sources */, 1CD9893E1ECFFC9E0014BBBF /* CAHALAudioStream.cpp in Sources */, 1CD9893F1ECFFC9E0014BBBF /* CAHALAudioSystemObject.cpp in Sources */, 1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */, 1CCC4F621E584100008053E4 /* BGMAppUITests.mm in Sources */, 1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */, 1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */, 19FE7921FD1B6C037429ECA4 /* BGMStatusBarItem.mm in Sources */, 19FE7DFF63F69E77C53BF95E /* BGMVolumeChangeListener.cpp in Sources */, 19FE7B32E1214BA0E8166A9E /* BGMMusic.m in Sources */, 19FE72D66CBC5C39F86333DE /* BGMPlayThroughRTLogger.cpp in Sources */, 19FE734C861E0370C21E4E94 /* BGMDebugLogging.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 27379B8B1C7F57DA0084A24C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 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 */, 19FE7590D7565E7677D84C55 /* BGMDebugLogging.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2743C9F21D86CFF90089613B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 1C62FE5123D3EB2E00B9B68E /* Mock_CAHALAudioSystemObject.cpp in Sources */, 1C8104AF22AD07E200B35517 /* BGMAppWatcher.m in Sources */, 1C8D830620423E2400A838F2 /* BGMSwinsian.m in Sources */, 1C227C0B1FA4C48200A95B6D /* BGMAppVolumes.m in Sources */, 1CACCF3B1F334450007F86CA /* BGMBackgroundMusicDevice.cpp in Sources */, 1C8D830C2042DE9600A838F2 /* BGMGooglePlayMusicDesktopPlayer.m in Sources */, 1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */, 27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */, 27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */, 27FB8C071DD75D0A0084DB9D /* BGMHermes.m in Sources */, 2743CA211D86DE780089613B /* BGMDeviceControlSync.cpp in Sources */, 1CE03A59239B56740036908D /* BGMDebugLoggingMenuItem.m 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 */, 2743CA101D86D7FA0089613B /* CADebugger.cpp in Sources */, 1C687A6B23B889E000834B75 /* BGMPlayThroughRTLoggerTests.mm in Sources */, 2743CA111D86D7FA0089613B /* CADebugMacros.cpp in Sources */, 1C62FE4F23D3EB2E00B9B68E /* Mock_CAHALAudioObject.cpp in Sources */, 2743CA121D86D7FA0089613B /* CADebugPrintf.cpp in Sources */, 1CCC4F4D1E581C40008053E4 /* BGMMusicPlayersUnitTests.mm in Sources */, 2743CA141D86D7FA0089613B /* CAHALAudioStream.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 */, 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 */, 1C62FE5223D3EB2E00B9B68E /* Mock_CAHALAudioDevice.cpp in Sources */, 1C62FE5323D3EB2E00B9B68E /* MockAudioDevice.cpp in Sources */, 2743CA091D86D41C0089613B /* BGMUserDefaults.m in Sources */, 1C62FE5023D3EB2E00B9B68E /* MockAudioObjects.cpp in Sources */, 1CC6593E1F91DEB400B0CCDC /* BGMTermination.mm in Sources */, 2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */, 1C62FE4E23D3EB2E00B9B68E /* MockAudioObject.cpp in Sources */, 2743CA021D86D3CB0089613B /* BGMiTunes.m in Sources */, 19FE77608F6C80D0B1F595A7 /* BGMStatusBarItem.mm in Sources */, 19FE7071FF5280BC38F35E1D /* BGMVolumeChangeListener.cpp in Sources */, 1C9258492090287F00B8D3A6 /* BGMGooglePlayMusicDesktopPlayerConnection.m in Sources */, 19FE76F614F260F3F65AF550 /* BGMMusic.m in Sources */, 19FE715E7338035C7BCD24E7 /* BGMPlayThroughRTLogger.cpp in Sources */, 19FE78EEC6D3C3B19D1FBD64 /* BGMDebugLogging.c in Sources */, 19FE7BD48C0CA2CAF16C9ACE /* BGMPlayThroughTests.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 278D71E81CABB6FF00899CF9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 1CCC4F3E1E58196C008053E4 /* BGMXPCHelperTests.m in Sources */, 19FE7C144C12607D947EB030 /* BGMDebugLogging.c in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 1CCC4F5A1E584081008053E4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 1CB8B3351BBA75EF000E2DD1 /* Background Music */; targetProxy = 1CCC4F591E584081008053E4 /* PBXContainerItemProxy */; }; 278D71F41CABB88B00899CF9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 27379B8E1C7F57DA0084A24C /* BGMXPCHelper */; targetProxy = 278D71F31CABB88B00899CF9 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ 1CB8B3421BBA75EF000E2DD1 /* MainMenu.xib */ = { isa = PBXVariantGroup; children = ( 1CB8B3431BBA75EF000E2DD1 /* Base */, ); name = MainMenu.xib; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 1C4603C51C05963C00150F9B /* DebugOpt */ = { isa = XCBuildConfiguration; buildSettings = { 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; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; CLANG_CXX_LIBRARY = "libc++"; 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; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = 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; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = c11; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = s; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "CoreAudio_Debug=1", "CoreAudio_UseSysLog=1", "CoreAudio_StopOnAssert=1", "CoreAudio_ThreadStampMessages=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SHADOW = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.13; 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"; }; name = DebugOpt; }; 1C4603C61C05963C00150F9B /* DebugOpt */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_USE_OPTIMIZATION_PROFILE = NO; CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES; CODE_SIGN_ENTITLEMENTS = "BGMApp/BGMApp-Debug.entitlements"; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_POSTPROCESSING = NO; DWARF_DSYM_FILE_NAME = "$(EXECUTABLE_NAME).dSYM"; DWARF_DSYM_FOLDER_PATH = "$(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)"; GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = BGMApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.App; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_STYLE = "non-global"; WARNING_CFLAGS = ( "-Wpartial-availability", "-Wthread-safety", ); }; name = DebugOpt; }; 1CB8B3511BBA75F0000E2DD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { 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; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; CLANG_CXX_LIBRARY = "libc++"; 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; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = 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; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; DEAD_CODE_STRIPPING = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = c11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "CoreAudio_Debug=1", "CoreAudio_UseSysLog=1", "CoreAudio_StopOnAssert=1", "CoreAudio_ThreadStampMessages=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SHADOW = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.13; 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"; }; name = Debug; }; 1CB8B3521BBA75F0000E2DD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { 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; CLANG_ANALYZER_SECURITY_INSECUREAPI_STRCPY = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; CLANG_CXX_LIBRARY = "libc++"; 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; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_IMPLICIT_SIGN_CONVERSION = NO; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = NO; CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = 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; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__EXIT_TIME_DESTRUCTORS = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = c11; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = s; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=0", "CoreAudio_Debug=0", "CoreAudio_StopOnAssert=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", ); GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES; GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; GCC_WARN_SHADOW = YES; GCC_WARN_STRICT_SELECTOR_MATCH = YES; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.13; MTL_ENABLE_DEBUG_INFO = NO; OTHER_CFLAGS = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = macosx; WARNING_CFLAGS = "-Wpartial-availability"; }; name = Release; }; 1CB8B3541BBA75F0000E2DD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_USE_OPTIMIZATION_PROFILE = NO; CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES; CODE_SIGN_ENTITLEMENTS = "BGMApp/BGMApp-Debug.entitlements"; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_POSTPROCESSING = NO; DWARF_DSYM_FILE_NAME = "$(EXECUTABLE_NAME).dSYM"; DWARF_DSYM_FOLDER_PATH = "$(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)"; GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = BGMApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.App; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_STYLE = "non-global"; WARNING_CFLAGS = ( "-Wpartial-availability", "-Wthread-safety", ); }; name = Debug; }; 1CB8B3551BBA75F0000E2DD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_USE_OPTIMIZATION_PROFILE = NO; CLANG_WARN_ATOMIC_IMPLICIT_SEQ_CST = YES; CODE_SIGN_ENTITLEMENTS = BGMApp/BGMApp.entitlements; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEPLOYMENT_POSTPROCESSING = YES; DWARF_DSYM_FILE_NAME = "$(EXECUTABLE_NAME).dSYM"; DWARF_DSYM_FOLDER_PATH = "$(CONFIGURATION_BUILD_DIR)/$(EXECUTABLE_FOLDER_PATH)"; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=0", "CoreAudio_Debug=0", "CoreAudio_StopOnAssert=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", "CoreAudio_UseSysLog=1", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; INFOPLIST_FILE = BGMApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; LLVM_LTO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.App; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_STYLE = "non-global"; WARNING_CFLAGS = ( "-Wno-profile-instr-out-of-date", "-Wpartial-availability", "-Wthread-safety", ); }; name = Release; }; 1CCC4F5C1E584081008053E4 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "BGMApp/BGMApp-Debug.entitlements"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "BGMAppTests/UITests/BGMAppUITests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.AppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; TEST_TARGET_NAME = "Background Music"; WARNING_CFLAGS = ""; }; name = Debug; }; 1CCC4F5D1E584081008053E4 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "BGMApp/BGMApp-Debug.entitlements"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "BGMAppTests/UITests/BGMAppUITests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.AppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; TEST_TARGET_NAME = "Background Music"; WARNING_CFLAGS = ""; }; name = Release; }; 1CCC4F5E1E584081008053E4 /* DebugOpt */ = { isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_ENTITLEMENTS = "BGMApp/BGMApp-Debug.entitlements"; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = "BGMAppTests/UITests/BGMAppUITests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.AppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; TEST_TARGET_NAME = "Background Music"; WARNING_CFLAGS = ""; }; name = DebugOpt; }; 27379B991C7F57DB0084A24C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { COPY_PHASE_STRIP = NO; DEPLOYMENT_POSTPROCESSING = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "CoreAudio_Debug=1", "CoreAudio_UseSysLog=1", "CoreAudio_StopOnAssert=1", ); INFOPLIST_FILE = BGMXPCHelper/Info.plist; INSTALL_GROUP = wheel; INSTALL_OWNER = root; INSTALL_PATH = /usr/local/libexec; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelper; PRODUCT_NAME = BGMXPCHelper; STRIP_INSTALLED_PRODUCT = NO; }; name = Debug; }; 27379B9A1C7F57DB0084A24C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { COPY_PHASE_STRIP = NO; DEPLOYMENT_POSTPROCESSING = YES; INFOPLIST_FILE = BGMXPCHelper/Info.plist; INSTALL_GROUP = wheel; INSTALL_OWNER = root; INSTALL_PATH = /usr/local/libexec; LLVM_LTO = YES; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelper; PRODUCT_NAME = BGMXPCHelper; STRIP_INSTALLED_PRODUCT = NO; }; name = Release; }; 27379B9B1C7F57DB0084A24C /* DebugOpt */ = { isa = XCBuildConfiguration; buildSettings = { COPY_PHASE_STRIP = NO; DEPLOYMENT_POSTPROCESSING = NO; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "CoreAudio_Debug=1", "CoreAudio_UseSysLog=1", "CoreAudio_StopOnAssert=1", ); INFOPLIST_FILE = BGMXPCHelper/Info.plist; INSTALL_GROUP = wheel; INSTALL_OWNER = root; INSTALL_PATH = /usr/local/libexec; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelper; PRODUCT_NAME = BGMXPCHelper; STRIP_INSTALLED_PRODUCT = NO; }; name = DebugOpt; }; 2743C9FE1D86CFFA0089613B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "CoreAudio_Debug=1", "CoreAudio_UseSysLog=1", "CoreAudio_StopOnAssert=1", "CoreAudio_ThreadStampMessages=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", "BGM_UnitTest=1", ); INFOPLIST_FILE = "BGMAppTests/UnitTests/BGMAppUnitTests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.AppUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; WARNING_CFLAGS = ""; }; name = Debug; }; 2743C9FF1D86CFFA0089613B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=0", "CoreAudio_Debug=0", "CoreAudio_StopOnAssert=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", "BGM_UnitTest=1", ); INFOPLIST_FILE = "BGMAppTests/UnitTests/BGMAppUnitTests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.AppUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; WARNING_CFLAGS = ""; }; name = Release; }; 2743CA001D86CFFA0089613B /* DebugOpt */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ADDRESS_SANITIZER_CONTAINER_OVERFLOW = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_SUSPICIOUS_MOVES = YES; CLANG_WARN__EXIT_TIME_DESTRUCTORS = NO; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "CoreAudio_Debug=1", "CoreAudio_UseSysLog=1", "CoreAudio_StopOnAssert=1", "CoreAudio_ThreadStampMessages=0", "BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)", "BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)", "CoreAudio_StopOnThrow=0", "BGM_UnitTest=1", ); INFOPLIST_FILE = "BGMAppTests/UnitTests/BGMAppUnitTests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.AppUnitTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; WARNING_CFLAGS = ""; }; name = DebugOpt; }; 278D71EE1CABB6FF00899CF9 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "BGMXPCHelperTests/BGMXPCHelperTests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelperTests; PRODUCT_NAME = "$(TARGET_NAME)"; WARNING_CFLAGS = ""; }; name = Debug; }; 278D71EF1CABB6FF00899CF9 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "BGMXPCHelperTests/BGMXPCHelperTests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelperTests; PRODUCT_NAME = "$(TARGET_NAME)"; WARNING_CFLAGS = ""; }; name = Release; }; 278D71F01CABB6FF00899CF9 /* DebugOpt */ = { isa = XCBuildConfiguration; buildSettings = { INFOPLIST_FILE = "BGMXPCHelperTests/BGMXPCHelperTests-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.bearisdriving.BGM.XPCHelperTests; PRODUCT_NAME = "$(TARGET_NAME)"; WARNING_CFLAGS = ""; }; name = DebugOpt; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1CB8B3311BBA75EF000E2DD1 /* Build configuration list for PBXProject "BGMApp" */ = { isa = XCConfigurationList; buildConfigurations = ( 1CB8B3511BBA75F0000E2DD1 /* Debug */, 1CB8B3521BBA75F0000E2DD1 /* Release */, 1C4603C51C05963C00150F9B /* DebugOpt */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1CB8B3531BBA75F0000E2DD1 /* Build configuration list for PBXNativeTarget "Background Music" */ = { isa = XCConfigurationList; buildConfigurations = ( 1CB8B3541BBA75F0000E2DD1 /* Debug */, 1CB8B3551BBA75F0000E2DD1 /* Release */, 1C4603C61C05963C00150F9B /* DebugOpt */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 1CCC4F5B1E584081008053E4 /* Build configuration list for PBXNativeTarget "BGMAppUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( 1CCC4F5C1E584081008053E4 /* Debug */, 1CCC4F5D1E584081008053E4 /* Release */, 1CCC4F5E1E584081008053E4 /* DebugOpt */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 27379B981C7F57DB0084A24C /* Build configuration list for PBXNativeTarget "BGMXPCHelper" */ = { isa = XCConfigurationList; buildConfigurations = ( 27379B991C7F57DB0084A24C /* Debug */, 27379B9A1C7F57DB0084A24C /* Release */, 27379B9B1C7F57DB0084A24C /* DebugOpt */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2743C9FD1D86CFFA0089613B /* Build configuration list for PBXNativeTarget "BGMAppUnitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 2743C9FE1D86CFFA0089613B /* Debug */, 2743C9FF1D86CFFA0089613B /* Release */, 2743CA001D86CFFA0089613B /* DebugOpt */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 278D71ED1CABB6FF00899CF9 /* Build configuration list for PBXNativeTarget "BGMXPCHelperTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 278D71EE1CABB6FF00899CF9 /* Debug */, 278D71EF1CABB6FF00899CF9 /* Release */, 278D71F01CABB6FF00899CF9 /* DebugOpt */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 1CB8B32E1BBA75EF000E2DD1 /* Project object */; } ================================================ FILE: BGMApp/BGMApp.xcodeproj/xcshareddata/xcschemes/BGMXPCHelper.xcscheme ================================================ ================================================ FILE: BGMApp/BGMApp.xcodeproj/xcshareddata/xcschemes/Background Music.xcscheme ================================================ ================================================ FILE: BGMApp/BGMAppTests/UITests/BGMApp.h ================================================ /* * BGMApp.h * * Generated with * sdef "/Applications/Background Music.app" | sdp -fh --basename BGMApp */ #import #import @class BGMAppOutputDevice, BGMAppApplication; /* * Background Music */ // A hardware device that can play audio @interface BGMAppOutputDevice : SBObject @property (copy, readonly) NSString *name; // The name of the output device. @property BOOL selected; // Is this the device to be used for audio output? @end // The application program @interface BGMAppApplication : SBApplication - (SBElementArray *) outputDevices; @property (copy) BGMAppOutputDevice *selectedOutputDevice; // The device to be used for audio output @end ================================================ FILE: BGMApp/BGMAppTests/UITests/BGMAppUITests-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: BGMApp/BGMAppTests/UITests/BGMAppUITests.mm ================================================ // 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 . // // BGMAppUITests.mm // BGMAppUITests // // Copyright © 2017, 2018, 2020, 2022 Kyle Neideck // // You might want to use Xcode's UI test recording feature if you add new tests. // // Local Includes #import "BGM_TestUtils.h" #import "BGM_Types.h" #import "BGMBackgroundMusicDevice.h" // Scripting Bridge Includes #import "BGMApp.h" // System Includes #import // TODO: Mock BGMDevice and music players. #if __clang_major__ >= 9 @interface BGMAppUITests : XCTestCase @end @implementation BGMAppUITests { // The BGMApp instance. XCUIApplication* app; // Convenience vars. // // The menu bar icon. (Called the status bar icon in some places.) XCUIElement* icon; // The menu items in the main menu. XCUIElementQuery* menuItems; // The Preferences menu item. XCUIElement* prefs; } - (void) setUp { [super setUp]; // In UI tests it is usually best to stop immediately when a failure occurs. self.continueAfterFailure = NO; // Set up the app object and some convenience vars. app = [[XCUIApplication alloc] init]; menuItems = app.menuBars.menuItems; prefs = menuItems[@"Preferences"]; icon = [app.menuBars childrenMatchingType:XCUIElementTypeStatusItem].element; // TODO: Make sure BGMDevice isn't set as the OS X default device before launching BGMApp. // Tell BGMApp not to load/store user defaults (settings) and to use // NSApplicationActivationPolicyRegular. If it used the "accessory" policy as usual, the tests // would fail to start because of a bug in Xcode. app.launchArguments = @[ @"--no-persistent-data", @"--show-dock-icon" ]; // Make the "Background Music wants to use the microphone" dialog appear every time so the test // doesn't need logic to handle both cases. // TODO: Commented out until acceptMicrophoneAuthorizationDialog work again. See below. // if (@available(macOS 10.15.4, *)) { // [app resetAuthorizationStatusForResource:XCUIProtectedResourceMicrophone]; // } // Launch BGMApp. [app launch]; // TODO: This doesn't seem to be working on macOS 12.4 (21F79). You can click OK manually for // now. // [self acceptMicrophoneAuthorizationDialog]; if (![icon waitForExistenceWithTimeout:20.0]) { // The status bar icon/button has this type when using older versions of XCTest, so try // both. (Actually, it might depend on the macOS or Xcode version. I'm not sure.) XCUIElement* iconOldType = [app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element; if ([iconOldType waitForExistenceWithTimeout:20.0]) { NSLog(@"icon = iconOldType"); icon = iconOldType; } } // Wait for the initial elements. XCTAssert([app waitForExistenceWithTimeout:20.0]); XCTAssert([icon waitForExistenceWithTimeout:20.0]); } // Clicks the OK button in the "Background Music wants to use the microphone" dialog. - (void) acceptMicrophoneAuthorizationDialog { XCUIApplication* unc = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.UserNotificationCenter"]; NSLog(@"UserNotificationCenter: %@", unc); XCUIElement* okButton = unc.dialogs.buttons[@"OK"]; XCTAssert([okButton waitForExistenceWithTimeout:20.0]); // This click is failing on GH Actions. No idea why, so try a sleep. (void)[XCTWaiter waitForExpectations:@[[XCTestExpectation new]] timeout:5.0]; [okButton click]; int retries = 10; while (retries > 0 && [okButton waitForExistenceWithTimeout:3.0]) { NSLog(@"Microphone authorization dialog is still open. Trying to click OK again."); [okButton click]; retries--; } } - (void) tearDown { // Click the quit menu item. if (!menuItems.count) { [icon click]; } [menuItems[@"Quit Background Music"] click]; // BGMApp should quit. XCTAssertTrue([app waitForState:XCUIApplicationStateNotRunning timeout:10.0]); [super tearDown]; } - (void) testCycleOutputDevices { const int NUM_CYCLES = 1; // sbApp lets us use AppleScript to query BGMApp and check the test has made the changes to its // settings we expect. BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID]; // Get macOS to show the "'Xcode' wants to control 'Background Music'" dialog before we start // the test so it doesn't interrupt it. [[sbApp selectedOutputDevice] name]; // Click the icon to open the main menu. [icon click]; // Get the list of output devices from the main menu. // BGMOutputDeviceMenuSection::createMenuItemForDevice gives every output device menu item the // accessibility identifier "output-device" so we can find all of them here. NSArray* outputDeviceMenuItems = [menuItems matchingIdentifier:@"output-device"].allElementsBoundByIndex; // For debugging certain issues, it can be useful to repeatedly switch between two // devices: // outputDeviceMenuItems = [outputDeviceMenuItems subarrayWithRange:NSMakeRange(0,2)]; XCTAssertGreaterThan(outputDeviceMenuItems.count, 0); // Click the last device to close the menu again. [outputDeviceMenuItems.lastObject click]; for (int i = 0; i < NUM_CYCLES; i++) { // Select each output device. for (XCUIElement* item in outputDeviceMenuItems) { [icon click]; [item click]; // Assert that the device we clicked is the selected device now. for (BGMAppOutputDevice* device in [sbApp outputDevices]) { // TODO: This seems a bit fragile. Would it still work with long device names? if ([device.name isEqualToString:[item title]]) { XCTAssert(device.selected); } else { XCTAssertFalse(device.selected); } } } } } - (void) testSelectMusicPlayer { // Select VLC as the music player. [icon click]; [prefs hover]; [prefs.menuItems[@"VLC"] click]; // The name of the Auto-pause menu item should change. Also check the accessibility identifier. [icon click]; XCTAssertEqualObjects(menuItems[@"Auto-pause VLC"].identifier, @"Auto-pause enabled"); // Select iTunes as the music player. [prefs hover]; [prefs.menuItems[@"iTunes"] click]; // The name of the Auto-pause menu item should change back. [icon click]; XCTAssert(menuItems[@"Auto-pause iTunes"].exists); } - (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 #endif /* __clang_major__ >= 9 */ ================================================ FILE: BGMApp/BGMAppTests/UITests/skip-ui-tests.py ================================================ #!/usr/bin/env python3 # 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 . # # skip-ui-tests.py # BGMAppUITests # # Copyright (c) 2017, 2024 Kyle Neideck # # Disables the UI tests. This is mainly useful on CI systems that don't support UI tests. # import xml.etree.ElementTree as ET SCHEME_FILE = "BGMApp/BGMApp.xcodeproj/xcshareddata/xcschemes/Background Music.xcscheme" UI_REF_XPATH = ".//BuildableReference[@BlueprintName='BGMAppUITests']/.." # Parse the Xcode scheme. tree = ET.parse(SCHEME_FILE) # Set the TestableReference for the UI tests to skipped. tree.getroot().findall(UI_REF_XPATH)[0].set("skipped", "YES") # Save the scheme. tree.write(SCHEME_FILE) ================================================ FILE: BGMApp/BGMAppTests/UnitTests/BGMAppUnitTests-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: BGMApp/BGMAppTests/UnitTests/BGMMusicPlayersUnitTests.mm ================================================ // 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 . // // BGMMusicPlayersUnitTests.mm // BGMAppUnitTests // // Copyright © 2016-2020 Kyle Neideck // // Unit include #import "BGMMusicPlayers.h" // BGM includes #import "BGM_Types.h" #import "BGMAudioDeviceManager.h" #import "BGMiTunes.h" #import "BGMDecibel.h" #import "BGMSpotify.h" #import "BGMVLC.h" // Local includes #import "BGM_TestUtils.h" #import "MockAudioObject.h" #import "MockAudioObjects.h" // System includes #import #import // 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 @property NSUUID* selectedPlayerID; @end @implementation BGMMockUserDefaults - (void) registerDefaults { } - (NSString* __nullable) selectedMusicPlayerID { return [self.selectedPlayerID UUIDString]; } - (void) setSelectedMusicPlayerID:(NSString* __nullable)selectedMusicPlayerID { #pragma unused (selectedMusicPlayerID) } - (BOOL) autoPauseMusicEnabled { return YES; } - (void) setAutoPauseMusicEnabled:(BOOL)autoPauseMusicEnabled { #pragma unused (autoPauseMusicEnabled) } @end // ------------------------------------------------------------------------------------------------- @interface BGMMockAudioDeviceManager : BGMAudioDeviceManager @end @implementation BGMMockAudioDeviceManager { BGMBackgroundMusicDevice bgmDevice; } - (BGMBackgroundMusicDevice) bgmDevice { return bgmDevice; } @end // ------------------------------------------------------------------------------------------------- @interface BGMMusicPlayersUnitTests : XCTestCase @end @implementation BGMMusicPlayersUnitTests { BGMAudioDeviceManager* devices; BGMMockUserDefaults* defaults; NSUUID* spotifyID; NSUUID* vlcID; } - (void) setUp { [super setUp]; // Mock BGMDevice. MockAudioObjects::CreateMockDevice(kBGMDeviceUID); MockAudioObjects::CreateMockDevice(kBGMDeviceUID_UISounds); devices = [BGMMockAudioDeviceManager new]; defaults = [BGMMockUserDefaults new]; // These are the IDs hardcoded in BGMSpotify and BGMVLC. spotifyID = [[NSUUID alloc] initWithUUIDString:@"EC2A907F-8515-4687-9570-1BF63176E6D8"]; vlcID = [[NSUUID alloc] initWithUUIDString:@"5226F4B9-C740-4045-A273-4B8EABC0E8FC"]; } - (void) tearDown { [super tearDown]; MockAudioObjects::DestroyMocks(); } - (void) testNoSelectedMusicPlayerStored_iTunesDefault { // Test the case where the user has never changed the music player preference. // Test with iTunes as the default. BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID] musicPlayerClasses:@[ BGMiTunes.class, BGMVLC.class ] userDefaults:defaults]; XCTAssertEqual(players.musicPlayers.count, 2); for (id player in players.musicPlayers) { XCTAssertTrue([player isKindOfClass:BGMiTunes.class] || [player isKindOfClass:BGMVLC.class]); } XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, [BGMiTunes sharedMusicPlayerID]); XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"iTunes"); } - (void) testNoSelectedMusicPlayerStored_vlcDefault { // Test the case where the user has never changed the music player preference. // Test with VLC as the default. BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices defaultMusicPlayerID:vlcID musicPlayerClasses:@[ BGMiTunes.class, BGMVLC.class, BGMDecibel.class ] userDefaults:defaults]; XCTAssertEqual(players.musicPlayers.count, 3); for (id player in players.musicPlayers) { XCTAssertTrue([player isKindOfClass:BGMiTunes.class] || [player isKindOfClass:BGMVLC.class] || [player isKindOfClass:BGMDecibel.class]); } XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, vlcID); XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"VLC"); } - (void) testSelectedMusicPlayerInUserDefaults { defaults.selectedPlayerID = spotifyID; BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices defaultMusicPlayerID:[BGMiTunes sharedMusicPlayerID] musicPlayerClasses:@[ BGMiTunes.class, BGMVLC.class, BGMSpotify.class ] userDefaults:defaults]; XCTAssertEqual(players.musicPlayers.count, 3); XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, spotifyID); XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"Spotify"); } - (void) testUnrecognizedSelectedMusicPlayerInUserDefaults { // If there's an unrecognized ID in user defaults, the default music player should be selected. defaults.selectedPlayerID = [[NSUUID alloc] initWithUUIDString:@"11111111-1111-1111-0000-000000000000"]; // This initializer sets iTunes as the default music player and adds all the other music players. BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices userDefaults:defaults]; XCTAssert(players.musicPlayers.count >= 6); XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, [BGMiTunes sharedMusicPlayerID]); XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"iTunes"); } - (void) testSelectedMusicPlayerInBGMDeviceProperties { // When it doesn't find a selected music player in user defaults, it should check BGMDevice's music // player properties. [devices bgmDevice].SetMusicPlayerBundleID(CFSTR("org.videolan.vlc")); BGMMusicPlayers* players = [[BGMMusicPlayers alloc] initWithAudioDevices:devices userDefaults:defaults]; XCTAssert(players.musicPlayers.count >= 6); XCTAssertEqualObjects(players.selectedMusicPlayer.musicPlayerID, vlcID); XCTAssertEqualObjects(players.selectedMusicPlayer.name, @"VLC"); } // TODO: Test setting the selectedMusicPlayer property @end ================================================ FILE: BGMApp/BGMAppTests/UnitTests/BGMPlayThroughRTLoggerTests.mm ================================================ // 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 . // // BGMPlayThroughRTLoggerTests.mm // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // // Unit Include #import "BGMPlayThroughRTLogger.h" // PublicUtility Includes #import "CARingBuffer.h" // System Includes #import #import @interface BGMPlayThroughRTLoggerTests : XCTestCase @end @implementation BGMPlayThroughRTLoggerTests { BGMPlayThroughRTLogger* logger; } - (void) setUp { [super setUp]; logger = new BGMPlayThroughRTLogger; } - (void) tearDown { [super tearDown]; delete logger; } - (void) testLogReleasingWaitingThreads { logger->LogReleasingWaitingThreads(); [self assertLoggedOneDebugMessage]; } - (void) testLogIfMachError_ReleaseWaitingThreadsSignal { logger->LogIfMachError_ReleaseWaitingThreadsSignal(KERN_SUCCESS); [self assertLoggedNoMessages]; } - (void) testLogIfDroppedFrames_didNotDropFrames { logger->LogIfDroppedFrames(11256.0, 11256.0); [self assertLoggedNoMessages]; } - (void) testLogIfDroppedFrames_didDropFrames { logger->LogIfDroppedFrames(11256.0, 11768.0); [self assertLoggedOneDebugMessage]; } - (void) testLogNoSamplesReady { logger->LogNoSamplesReady(512, 1024, 512.0); [self assertLoggedOneDebugMessage]; } - (void) testLogExceptionStoppingIOProc_withoutErrorCode { // Set a test-only flag that keeps it from calling abort() after logging the error when the // tests are compiled with the debug configuration. logger->mContinueOnErrorLogged = true; logger->LogExceptionStoppingIOProc("InputDeviceIOProc"); [self assertLoggedOneErrorMessage]; } - (void) testLogExceptionStoppingIOProc_withErrorCode { // Set a test-only flag that keeps it from calling abort() after logging the error when the // tests are compiled with the debug configuration. logger->mContinueOnErrorLogged = true; logger->LogExceptionStoppingIOProc("OutputDeviceIOProc", kAudioHardwareUnknownPropertyError); [self assertLoggedOneErrorMessage]; } - (void) testLogUnexpectedIOStateAfterStopping { logger->LogUnexpectedIOStateAfterStopping("OutputDeviceIOProc", 1); [self assertLoggedOneWarningMessage]; } - (void) testLogRingBufferUnavailable { logger->LogRingBufferUnavailable("OutputDeviceIOProc", false); [self assertLoggedOneWarningMessage]; } - (void) testLogIfRingBufferError_Fetch_noError { logger->LogIfRingBufferError_Fetch(kCARingBufferError_OK); [self assertLoggedNoMessages]; } - (void) testLogIfRingBufferError_Fetch_errorCPUOverload { logger->LogIfRingBufferError_Fetch(kCARingBufferError_CPUOverload); [self assertLoggedOneWarningMessage]; } - (void) testLogIfRingBufferError_Fetch_errorTooMuch { // Set a test-only flag that keeps it from calling abort() after logging the error when the // tests are compiled with the debug configuration. logger->mContinueOnErrorLogged = true; logger->LogIfRingBufferError_Fetch(kCARingBufferError_TooMuch); [self assertLoggedOneErrorMessage]; } - (void) testLogIfRingBufferError_Store_noError { logger->LogIfRingBufferError_Store(kCARingBufferError_OK); [self assertLoggedNoMessages]; } - (void) testLogIfRingBufferError_Store_errorCPUOverload { logger->LogIfRingBufferError_Store(kCARingBufferError_CPUOverload); [self assertLoggedOneWarningMessage]; } - (void) testLogIfRingBufferError_Store_errorTooMuch { // Set a test-only flag that keeps it from calling abort() after logging the error when the // tests are compiled with the debug configuration. logger->mContinueOnErrorLogged = true; logger->LogIfRingBufferError_Store(kCARingBufferError_TooMuch); [self assertLoggedOneErrorMessage]; } - (void) waitForLoggingThread { // Wait for it to finish logging the messages. bool noMessagesLeft = logger->WaitUntilLoggerThreadIdle(); XCTAssert(noMessagesLeft); } - (void) assertLoggedNoMessages { [self waitForLoggingThread]; XCTAssertEqual(0, logger->mNumDebugMessagesLogged); XCTAssertEqual(0, logger->mNumWarningMessagesLogged); XCTAssertEqual(0, logger->mNumErrorMessagesLogged); } - (void) assertLoggedOneDebugMessage { [self waitForLoggingThread]; XCTAssertEqual(1, logger->mNumDebugMessagesLogged); XCTAssertEqual(0, logger->mNumWarningMessagesLogged); XCTAssertEqual(0, logger->mNumErrorMessagesLogged); } - (void) assertLoggedOneWarningMessage { [self waitForLoggingThread]; XCTAssertEqual(0, logger->mNumDebugMessagesLogged); XCTAssertEqual(1, logger->mNumWarningMessagesLogged); XCTAssertEqual(0, logger->mNumErrorMessagesLogged); } - (void) assertLoggedOneErrorMessage { [self waitForLoggingThread]; XCTAssertEqual(0, logger->mNumDebugMessagesLogged); XCTAssertEqual(0, logger->mNumWarningMessagesLogged); XCTAssertEqual(1, logger->mNumErrorMessagesLogged); } @end ================================================ FILE: BGMApp/BGMAppTests/UnitTests/BGMPlayThroughTests.mm ================================================ // 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 . // // BGMPlayThroughTests.mm // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // // Unit Include #import "BGMPlayThrough.h" // Local Includes #import "MockAudioDevice.h" #import "MockAudioObjects.h" // BGM Includes #import "BGM_Types.h" #import "BGMAudioDevice.h" // STL Includes #import // System Includes #import @interface BGMPlayThroughTests : XCTestCase @end @implementation BGMPlayThroughTests { BGMAudioDevice inputDevice; BGMAudioDevice outputDevice; // The unit tests use mock implementations of CAHALAudioObject and CAHALAudioDevice, which are // the superclasses of BGMAudioDevice. When BGMPlayThrough calls methods on inputDevice or // outputDevice, some of them will update these objects. std::shared_ptr mockInputDevice; std::shared_ptr mockOutputDevice; } - (void) setUp { [super setUp]; // Set up the mocks. mockInputDevice = MockAudioObjects::CreateMockDevice(kBGMDeviceUID); mockOutputDevice = MockAudioObjects::CreateMockDevice("Mock Output Device"); inputDevice = BGMAudioDevice(mockInputDevice->GetObjectID()); outputDevice = BGMAudioDevice(mockOutputDevice->GetObjectID()); } - (void) tearDown { [super tearDown]; MockAudioObjects::DestroyMocks(); } - (void) testActivate { // Set the mock output device's sample rate and IO buffer size. outputDevice.SetNominalSampleRate(12345.0); outputDevice.SetIOBufferSize(123); // Create an instance and activate it. BGMPlayThrough playThrough(inputDevice, outputDevice); playThrough.Activate(); // It should set the input device's sample rate and IO buffer size to match the output device. XCTAssertEqual(12345.0, inputDevice.GetNominalSampleRate()); XCTAssertEqual(123, inputDevice.GetIOBufferSize()); // It should add the property listeners it needs. std::set expectedProperties { kAudioDevicePropertyDeviceIsRunning, kAudioDeviceProcessorOverload, kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp }; XCTAssertEqual(expectedProperties, mockInputDevice->mPropertiesWithListeners); } - (void) testDeactivate { BGMPlayThrough playThrough(inputDevice, outputDevice); playThrough.Activate(); playThrough.Deactivate(); // It should remove the property listeners added by Activate. XCTAssert(mockInputDevice->mPropertiesWithListeners.empty()); } @end ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/MockAudioDevice.cpp ================================================ // 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 . // // MockAudioDevice.cpp // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // // Self Include #include "MockAudioDevice.h" // BGM Includes #include "BGM_Types.h" // STL Includes #include MockAudioDevice::MockAudioDevice(const std::string& inUID) : mUID(inUID), mNominalSampleRate(44100.0), mIOBufferSize(512), MockAudioObject(static_cast(std::hash{}(inUID))) { } CACFString MockAudioDevice::GetPlayerBundleID() const { if(mUID != kBGMDeviceUID) { throw "Only BGMDevice has kAudioDeviceCustomPropertyMusicPlayerBundleID"; } return mPlayerBundleID; } void MockAudioDevice::SetPlayerBundleID(const CACFString& inPlayerBundleID) { if(mUID != kBGMDeviceUID) { throw "Only BGMDevice has kAudioDeviceCustomPropertyMusicPlayerBundleID"; } mPlayerBundleID = inPlayerBundleID; } ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/MockAudioDevice.h ================================================ // 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 . // // MockAudioObject.h // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // #ifndef BGMAppUnitTests__MockAudioDevice #define BGMAppUnitTests__MockAudioDevice // Superclass Includes #include "MockAudioObject.h" // STL Includes #include /*! * A mock audio device in our mock CoreAudio HAL. In the HAL's API class hierarchy, the base class * for audio devices, kAudioDeviceClassID, is the audio objects class, kAudioObjectClassID. * * The unit tests generally use instances of this class to verify the HAL is being queried correctly * and to control the responses that the code they're testing will receive from the mock HAL. */ class MockAudioDevice : public MockAudioObject { public: MockAudioDevice(const std::string& inUID); /*! * @return This device's music player bundle ID property. * @throws If this device isn't a mock of BGMDevice. */ CACFString GetPlayerBundleID() const; /*! * Set this device's music player bundle ID property. * @throws If this device isn't a mock of BGMDevice. */ void SetPlayerBundleID(const CACFString& inPlayerBundleID); /*! * The device's UID. The UID is a persistent token used to identify a particular audio device * across boot sessions. */ const std::string mUID; Float64 mNominalSampleRate; UInt32 mIOBufferSize; private: CACFString mPlayerBundleID { "" }; }; #endif /* BGMAppUnitTests__MockAudioDevice */ ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/MockAudioObject.cpp ================================================ // 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 . // // MockAudioObject.cpp // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // // Self Include #include "MockAudioObject.h" MockAudioObject::MockAudioObject(AudioObjectID inAudioObjectID) : mAudioObjectID(inAudioObjectID) { } AudioObjectID MockAudioObject::GetObjectID() const { return mAudioObjectID; } ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/MockAudioObject.h ================================================ // 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 . // // MockAudioObject.h // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // #ifndef BGMAppUnitTests__MockAudioObject #define BGMAppUnitTests__MockAudioObject // PublicUtility Includes #include "CACFString.h" // STL Includes #include // System Includes #include /*! * The base class for mock audio objects in our mock CoreAudio HAL. Maps to kAudioObjectClassID * (AudioHardwareBase.h) in the HAL's API class hierarchy. */ class MockAudioObject { public: MockAudioObject(AudioObjectID inAudioObjectID); virtual ~MockAudioObject() = default; AudioObjectID GetObjectID() const; /*! * The properties that callers have added listeners for (and haven't since removed). See * CAHALAudioObject::AddPropertyListener and CAHALAudioObject::RemovePropertyListener. */ std::set mPropertiesWithListeners; private: AudioObjectID mAudioObjectID; }; #endif /* BGMAppUnitTests__MockAudioObject */ ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/MockAudioObjects.cpp ================================================ // 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 . // // MockAudioObjects.cpp // BGMAppUnitTests // // Copyright © 2020, 2025 Kyle Neideck // // Self Include #include "MockAudioObjects.h" // PublicUtility Includes #include "CACFString.h" // STL Includes #include // static MockAudioObjects::MockDeviceMap MockAudioObjects::sDevices; // static MockAudioObjects::MockDeviceMapByUID MockAudioObjects::sDevicesByUID; // static std::shared_ptr MockAudioObjects::CreateMockDevice(const std::string& inUID) { std::shared_ptr mockDevice = std::make_shared(inUID); sDevices.insert(MockDeviceMap::value_type(mockDevice->GetObjectID(), mockDevice)); sDevicesByUID.insert(MockDeviceMapByUID::value_type(inUID, mockDevice)); return mockDevice; } // static void MockAudioObjects::DestroyMocks() { sDevices.clear(); } // static std::shared_ptr MockAudioObjects::GetAudioObject(AudioObjectID inAudioObjectID) { auto device = GetAudioDeviceOrNull(inAudioObjectID); if(device) { return device; } // Devices are the only audio objects we currently mock. // Tests have to create mocks for all of the audio objects they expect the code they test to // access. They should fail if it accesses any others. throw "Mock audio object not found."; } // static std::shared_ptr MockAudioObjects::GetAudioDevice(AudioObjectID inAudioObjectID) { auto device = GetAudioDeviceOrNull(inAudioObjectID); if(device) { return device; } // Tests have to create mocks for all of the audio devices they expect the code they test to // access. They should fail if it accesses any others. throw "Mock audio device not found."; } // static std::shared_ptr MockAudioObjects::GetAudioDevice(CFStringRef inUID) { // Convert inUID to a std::string. UInt32 uidCStringLen = CACFString::GetStringByteLength(inUID) + 1; std::vector uidCString(uidCStringLen); CACFString::GetCString(inUID, uidCString.data(), uidCStringLen); std::string uid = std::string(uidCString.data()); return GetAudioDevice(uid); } // static std::shared_ptr MockAudioObjects::GetAudioDevice(const std::string& inUID) { auto device = sDevicesByUID.find(inUID); if(device != sDevicesByUID.end()) { return device->second; } return nullptr; } // static std::shared_ptr MockAudioObjects::GetAudioDeviceOrNull(AudioObjectID inAudioObjectID) { auto device = sDevices.find(inAudioObjectID); if(device != sDevices.end()) { return device->second; } return nullptr; } ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/MockAudioObjects.h ================================================ // 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 . // // MockAudioObjects.h // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // #ifndef BGMAppUnitTests__MockAudioObjects #define BGMAppUnitTests__MockAudioObjects // Local Includes #include "MockAudioObject.h" #include "MockAudioDevice.h" // STL Includes #include #include #include // System Includes #include class MockAudioObjects { public: /*! * Create a mock audio device in the mock CoreAudio HAL. * * The mock device will then be accessible using GetAudioObject and GetAudioDevice. The * Mock_CAHAL* implementations will access the mock device when they query the mock HAL. * * Unit tests can check the mock device to verify the code they're testing has called the mocked * CAHAL classes correctly. They can also modify the mock device to control the Mock_CAHAL* * implementations, e.g. to have CAHALAudioDevice::IsAlive return false so the test can cover * the case where a device is being removed from the system. * * @param inUID The UID string to give the device. The UID is a persistent token used to * identify a particular audio device across boot sessions. * @return The mock device. */ static std::shared_ptr CreateMockDevice(const std::string& inUID); /*! * Remove all mock audio objects from the mock HAL. (Currently, mock devices are the only mock * objects that can be created.) */ static void DestroyMocks(); /*! Get a mock audio object by its ID. */ static std::shared_ptr GetAudioObject(AudioObjectID inAudioObjectID); /*! Get a mock audio device by its ID. */ static std::shared_ptr GetAudioDevice(AudioObjectID inAudioDeviceID); /*! Get a mock audio device by its UID. */ static std::shared_ptr GetAudioDevice(const std::string& inUID); /*! Get a mock audio device by its UID. */ static std::shared_ptr GetAudioDevice(CFStringRef inUID); private: typedef std::map> MockDeviceMap; typedef std::map> MockDeviceMapByUID; static std::shared_ptr GetAudioDeviceOrNull(AudioObjectID inAudioDeviceID); /*! Maps IDs to mocked audio devices. */ static MockDeviceMap sDevices; /*! Maps UIDs (ID strings) to mocked audio devices. */ static MockDeviceMapByUID sDevicesByUID; }; #endif /* BGMAppUnitTests__MockAudioObjects */ ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/Mock_CAHALAudioDevice.cpp ================================================ // 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 . // // Mock_CAHALAudioDevice.cpp // BGMAppUnitTests // // Copyright © 2020 Kyle Neideck // // Self Include #include "CAHALAudioDevice.h" // Local Includes #include "MockAudioDevice.h" #include "MockAudioObjects.h" // BGM Includes #include "BGM_Types.h" // PublicUtility Includes #include "CACFString.h" #include "CAHALAudioSystemObject.h" #include "CAPropertyAddress.h" #pragma clang diagnostic ignored "-Wunused-parameter" CAHALAudioDevice::CAHALAudioDevice(AudioObjectID inObjectID) : CAHALAudioObject(inObjectID) { } CAHALAudioDevice::CAHALAudioDevice(CFStringRef inUID) : CAHALAudioObject(CAHALAudioSystemObject().GetAudioDeviceForUID(inUID)) { } CAHALAudioDevice::~CAHALAudioDevice() { } void CAHALAudioDevice::GetCurrentVirtualFormats(bool inIsInput, UInt32& ioNumberStreams, AudioStreamBasicDescription* outFormats) const { ioNumberStreams = 1; CAPropertyAddress theAddress(kAudioStreamPropertyVirtualFormat); UInt32 theSize = sizeof(AudioStreamBasicDescription); GetPropertyData(theAddress, 0, NULL, theSize, outFormats); } UInt32 CAHALAudioDevice::GetIOBufferSize() const { return MockAudioObjects::GetAudioDevice(GetObjectID())->mIOBufferSize; } void CAHALAudioDevice::SetIOBufferSize(UInt32 inBufferSize) { MockAudioObjects::GetAudioDevice(GetObjectID())->mIOBufferSize = inBufferSize; } bool CAHALAudioDevice::IsAlive() const { return true; } AudioDeviceIOProcID CAHALAudioDevice::CreateIOProcID(AudioDeviceIOProc inIOProc, void* inClientData) { return reinterpret_cast(0x99990000); } void CAHALAudioDevice::DestroyIOProcID(AudioDeviceIOProcID inIOProcID) { } Float64 CAHALAudioDevice::GetNominalSampleRate() const { return MockAudioObjects::GetAudioDevice(GetObjectID())->mNominalSampleRate; } void CAHALAudioDevice::SetNominalSampleRate(Float64 inSampleRate) { MockAudioObjects::GetAudioDevice(GetObjectID())->mNominalSampleRate = inSampleRate; } CFStringRef CAHALAudioDevice::CopyDeviceUID() const { std::string uid = MockAudioObjects::GetAudioDevice(GetObjectID())->mUID; return CACFString(uid.c_str()).CopyCFString(); } #pragma mark Unimplemented Methods bool CAHALAudioDevice::HasModelUID() const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioDevice::CopyModelUID() const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioDevice::CopyConfigurationApplicationBundleID() const { Throw(new CAException(kAudio_UnimplementedError)); } CFURLRef CAHALAudioDevice::CopyIconLocation() const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetTransportType() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::CanBeDefaultDevice(bool inIsInput, bool inIsSystem) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasDevicePlugInStatus() const { Throw(new CAException(kAudio_UnimplementedError)); } OSStatus CAHALAudioDevice::GetDevicePlugInStatus() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::IsHidden() const { Throw(new CAException(kAudio_UnimplementedError)); } pid_t CAHALAudioDevice::GetHogModeOwner() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::IsHogModeSettable() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::TakeHogMode() { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::ReleaseHogMode() { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasPreferredStereoChannels(bool inIsInput) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetPreferredStereoChannels(bool inIsInput, UInt32& outLeft, UInt32& outRight) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetPreferredStereoChannels(bool inIsInput, UInt32 inLeft, UInt32 inRight) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasPreferredChannelLayout(bool inIsInput) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetPreferredChannelLayout(bool inIsInput, AudioChannelLayout& outChannelLayout) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetPreferredStereoChannels(bool inIsInput, AudioChannelLayout& inChannelLayout) { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetNumberRelatedAudioDevices() const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetRelatedAudioDevices(UInt32& ioNumberRelatedDevices, AudioObjectID* outRelatedDevices) const { Throw(new CAException(kAudio_UnimplementedError)); } AudioObjectID CAHALAudioDevice::GetRelatedAudioDeviceByIndex(UInt32 inIndex) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetNumberStreams(bool inIsInput) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetStreams(bool inIsInput, UInt32& ioNumberStreams, AudioObjectID* outStreamList) const { Throw(new CAException(kAudio_UnimplementedError)); } AudioObjectID CAHALAudioDevice::GetStreamByIndex(bool inIsInput, UInt32 inIndex) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetTotalNumberChannels(bool inIsInput) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetCurrentPhysicalFormats(bool inIsInput, UInt32& ioNumberStreams, AudioStreamBasicDescription* outFormats) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::IsRunning() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::IsRunningSomewhere() const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetLatency(bool inIsInput) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetSafetyOffset(bool inIsInput) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasClockDomain() const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetClockDomain() const { Throw(new CAException(kAudio_UnimplementedError)); } Float64 CAHALAudioDevice::GetActualSampleRate() const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetNumberAvailableNominalSampleRateRanges() const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetAvailableNominalSampleRateRanges(UInt32& ioNumberRanges, AudioValueRange* outRanges) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetAvailableNominalSampleRateRangeByIndex(UInt32 inIndex, Float64& outMinimum, Float64& outMaximum) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::IsValidNominalSampleRate(Float64 inSampleRate) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::IsIOBufferSizeSettable() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::UsesVariableIOBufferSizes() const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetMaximumVariableIOBufferSize() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasIOBufferSizeRange() const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetIOBufferSizeRange(UInt32& outMinimum, UInt32& outMaximum) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::StartIOProc(AudioDeviceIOProcID inIOProcID) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::StartIOProcAtTime(AudioDeviceIOProcID inIOProcID, AudioTimeStamp& ioStartTime, bool inIsInput, bool inIgnoreHardware) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::StopIOProc(AudioDeviceIOProcID inIOProcID) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetIOProcStreamUsage(AudioDeviceIOProcID inIOProcID, bool inIsInput, bool* outStreamUsage) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetIOProcStreamUsage(AudioDeviceIOProcID inIOProcID, bool inIsInput, const bool* inStreamUsage) { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetIOCycleUsage() const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetIOCycleUsage(Float32 inValue) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetCurrentTime(AudioTimeStamp& outTime) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::TranslateTime(const AudioTimeStamp& inTime, AudioTimeStamp& outTime) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetNearestStartTime(AudioTimeStamp& ioTime, bool inIsInput, bool inIgnoreHardware) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasVolumeControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::VolumeControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetVolumeControlScalarForDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetVolumeControlDecibelForScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasSubVolumeControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::SubVolumeControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetSubVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetSubVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetSubVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetSubVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetSubVolumeControlScalarForDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetSubVolumeControlDecibelForScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasMuteControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::MuteControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::GetMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasSoloControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::SoloControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::GetSoloControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetSoloControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasStereoPanControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::StereoPanControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } Float32 CAHALAudioDevice::GetStereoPanControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetStereoPanControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetStereoPanControlChannels(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& outLeftChannel, UInt32& outRightChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasJackControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::GetJackControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasSubMuteControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::SubMuteControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::GetSubMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetSubMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasiSubOwnerControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::iSubOwnerControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::GetiSubOwnerControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetiSubOwnerControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasDataSourceControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::DataSourceControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetCurrentDataSourceID(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetCurrentDataSourceByID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetNumberAvailableDataSources(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetAvailableDataSources(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& ioNumberSources, UInt32* outSources) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetAvailableDataSourceByIndex(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inIndex) const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioDevice::CopyDataSourceNameForID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasDataDestinationControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::DataDestinationControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetCurrentDataDestinationID(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetCurrentDataDestinationByID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetNumberAvailableDataDestinations(AudioObjectPropertyScope inScope, UInt32 inChannel) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetAvailableDataDestinations(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& ioNumberDestinations, UInt32* outDestinations) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetAvailableDataDestinationByIndex(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inIndex) const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioDevice::CopyDataDestinationNameForID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::HasClockSourceControl() const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioDevice::ClockSourceControlIsSettable() const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetCurrentClockSourceID() const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::SetCurrentClockSourceByID(UInt32 inID) { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetNumberAvailableClockSources() const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioDevice::GetAvailableClockSources(UInt32& ioNumberSources, UInt32* outSources) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetAvailableClockSourceByIndex(UInt32 inIndex) const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioDevice::CopyClockSourceNameForID(UInt32 inID) const { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioDevice::GetClockSourceKindForID(UInt32 inID) const { Throw(new CAException(kAudio_UnimplementedError)); } ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/Mock_CAHALAudioObject.cpp ================================================ // 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 . // // Mock_CAHALAudioObject.cpp // BGMAppUnitTests // // Copyright © 2016, 2020 Kyle Neideck // // Self Include #include "CAHALAudioObject.h" // Local Includes #include "MockAudioObjects.h" // BGM Includes #include "BGM_Types.h" // PublicUtility Includes #include "CACFString.h" #pragma clang diagnostic ignored "-Wunused-parameter" CAHALAudioObject::CAHALAudioObject(AudioObjectID inObjectID) : mObjectID(inObjectID) { } CAHALAudioObject::~CAHALAudioObject() { } AudioObjectID CAHALAudioObject::GetObjectID() const { return mObjectID; } void CAHALAudioObject::GetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32& ioDataSize, void* outData) const { switch(inAddress.mSelector) { case kAudioDeviceCustomPropertyMusicPlayerBundleID: *reinterpret_cast(outData) = MockAudioObjects::GetAudioDevice(GetObjectID())-> GetPlayerBundleID().CopyCFString(); break; case kAudioDevicePropertyStreams: reinterpret_cast(outData)[0] = 1; if(inAddress.mScope == kAudioObjectPropertyScopeGlobal) { reinterpret_cast(outData)[1] = 2; } break; case kAudioDevicePropertyBufferFrameSize: *reinterpret_cast(outData) = 512; break; case kAudioDevicePropertyDeviceIsAlive: *reinterpret_cast(outData) = 1; break; case kAudioStreamPropertyVirtualFormat: { AudioStreamBasicDescription* outASBD = reinterpret_cast(outData); outASBD->mSampleRate = 44100.0; outASBD->mFormatID = kAudioFormatLinearPCM; outASBD->mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked; outASBD->mBytesPerPacket = 8; outASBD->mFramesPerPacket = 1; outASBD->mBytesPerFrame = 8; outASBD->mChannelsPerFrame = 2; outASBD->mBitsPerChannel = 32; break; } default: Throw(new CAException(kAudio_UnimplementedError)); } } void CAHALAudioObject::SetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData) { switch(inAddress.mSelector) { case kAudioDeviceCustomPropertyMusicPlayerBundleID: MockAudioObjects::GetAudioDevice(GetObjectID())->SetPlayerBundleID( CACFString(*reinterpret_cast(inData), false)); break; default: break; } } UInt32 CAHALAudioObject::GetPropertyDataSize(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData) const { switch(inAddress.mSelector) { case kAudioDevicePropertyStreams: return (inAddress.mScope == kAudioObjectPropertyScopeGlobal ? 2 : 1) * sizeof(AudioObjectID); default: Throw(new CAException(kAudio_UnimplementedError)); } } void CAHALAudioObject::AddPropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData) { MockAudioObjects::GetAudioObject(GetObjectID())-> mPropertiesWithListeners.insert(inAddress.mSelector); } void CAHALAudioObject::RemovePropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData) { MockAudioObjects::GetAudioObject(GetObjectID())-> mPropertiesWithListeners.erase(inAddress.mSelector); } #pragma mark Unimplemented Methods void CAHALAudioObject::SetObjectID(AudioObjectID inObjectID) { Throw(new CAException(kAudio_UnimplementedError)); } AudioClassID CAHALAudioObject::GetClassID() const { Throw(new CAException(kAudio_UnimplementedError)); } AudioObjectID CAHALAudioObject::GetOwnerObjectID() const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioObject::CopyOwningPlugInBundleID() const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioObject::CopyName() const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioObject::CopyManufacturer() const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioObject::CopyNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioObject::CopyCategoryNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const { Throw(new CAException(kAudio_UnimplementedError)); } CFStringRef CAHALAudioObject::CopyNumberNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioObject::ObjectExists(AudioObjectID inObjectID) { Throw(new CAException(kAudio_UnimplementedError)); } UInt32 CAHALAudioObject::GetNumberOwnedObjects(AudioClassID inClass) const { Throw(new CAException(kAudio_UnimplementedError)); } void CAHALAudioObject::GetAllOwnedObjects(AudioClassID inClass, UInt32& ioNumberObjects, AudioObjectID* ioObjectIDs) const { Throw(new CAException(kAudio_UnimplementedError)); } AudioObjectID CAHALAudioObject::GetOwnedObjectByIndex(AudioClassID inClass, UInt32 inIndex) { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioObject::HasProperty(const AudioObjectPropertyAddress& inAddress) const { Throw(new CAException(kAudio_UnimplementedError)); } bool CAHALAudioObject::IsPropertySettable(const AudioObjectPropertyAddress& inAddress) const { Throw(new CAException(kAudio_UnimplementedError)); } ================================================ FILE: BGMApp/BGMAppTests/UnitTests/Mocks/Mock_CAHALAudioSystemObject.cpp ================================================ // 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 . // // Mock_CAHALAudioSystemObject.cpp // BGMAppUnitTests // // Copyright © 2017, 2020 Kyle Neideck // // Self include #include "CAHALAudioSystemObject.h" // BGM Includes #include "BGM_Types.h" // Local Includes #include "MockAudioObjects.h" CAHALAudioSystemObject::CAHALAudioSystemObject() : CAHALAudioObject(kAudioObjectSystemObject) { } CAHALAudioSystemObject::~CAHALAudioSystemObject() { } AudioObjectID CAHALAudioSystemObject::GetAudioDeviceForUID(CFStringRef inUID) const { auto device = MockAudioObjects::GetAudioDevice(inUID); if(device) { return device->GetObjectID(); } return kAudioObjectUnknown; } #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)); } ================================================ FILE: BGMApp/BGMThreadSafetyAnalysis.h ================================================ // 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 . // Original licence at the end of this file. // // BGMThreadSafetyAnalysis.h // PublicUtility // // © Copyright 2007-2020, The Clang Team // Copyright © 2020 Kyle Neideck // // Macros that wrap Clang's attributes for statically checking concurrency properties. From // . // #ifndef PublicUtility__BGMThreadSafetyAnalysis #define PublicUtility__BGMThreadSafetyAnalysis // Enable thread safety attributes only with clang. // The attributes can be safely erased when compiling with other compilers. #if defined(__clang__) && (!defined(SWIG)) #define THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x)) #else #define THREAD_ANNOTATION_ATTRIBUTE__(x) // no-op #endif #define CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(capability(x)) #define SCOPED_CAPABILITY \ THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable) #define GUARDED_BY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x)) #define PT_GUARDED_BY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x)) #define ACQUIRED_BEFORE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquired_before(__VA_ARGS__)) #define ACQUIRED_AFTER(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquired_after(__VA_ARGS__)) #define REQUIRES(...) \ THREAD_ANNOTATION_ATTRIBUTE__(requires_capability(__VA_ARGS__)) #define REQUIRES_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(requires_shared_capability(__VA_ARGS__)) #define ACQUIRE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquire_capability(__VA_ARGS__)) #define ACQUIRE_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(acquire_shared_capability(__VA_ARGS__)) #define RELEASE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(release_capability(__VA_ARGS__)) #define RELEASE_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(release_shared_capability(__VA_ARGS__)) #define TRY_ACQUIRE(...) \ THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_capability(__VA_ARGS__)) #define TRY_ACQUIRE_SHARED(...) \ THREAD_ANNOTATION_ATTRIBUTE__(try_acquire_shared_capability(__VA_ARGS__)) #define EXCLUDES(...) \ THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__)) #define ASSERT_CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(assert_capability(x)) #define ASSERT_SHARED_CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(assert_shared_capability(x)) #define RETURN_CAPABILITY(x) \ THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x)) #define NO_THREAD_SAFETY_ANALYSIS \ THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis) #endif /* PublicUtility__BGMThreadSafetyAnalysis */ /* This file is derived from "mutex.h" from the Clang documentation at . This is the original license of "mutex.h". ============================================================================== The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: ============================================================================== Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ---- LLVM Exceptions to the Apache 2.0 License ---- As an exception, if, as a result of your compiling your source code, portions of this Software are embedded into an Object form of such source code, you may redistribute such embedded portions in such Object form without complying with the conditions of Sections 4(a), 4(b) and 4(d) of the License. In addition, if you combine or link compiled forms of this Software with software that is licensed under the GPLv2 ("Combined Software") and if a court of competent jurisdiction determines that the patent provision (Section 3), the indemnity provision (Section 9) or other Section of the License conflicts with the conditions of the GPLv2, you may retroactively and prospectively choose to deem waived or otherwise exclude such Section(s) of the License, but only in their entirety and only with respect to the Combined Software. ============================================================================== Software from third parties included in the LLVM Project: ============================================================================== The LLVM Project contains third party software which is under different license terms. All such code will be identified clearly using at least one of two mechanisms: 1) It will be in a separate directory tree with its own `LICENSE.txt` or `LICENSE` file at the top containing the specific license and restrictions which apply to that software, or 2) It will contain specific license and restriction terms at the top of every file. ============================================================================== Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy): ============================================================================== University of Illinois/NCSA Open Source License Copyright (c) 2007-2019 University of Illinois at Urbana-Champaign. All rights reserved. Developed by: LLVM Team University of Illinois at Urbana-Champaign http://llvm.org Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal with the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimers. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimers in the documentation and/or other materials provided with the distribution. * Neither the names of the LLVM Team, University of Illinois at Urbana-Champaign, nor the names of its contributors may be used to endorse or promote products derived from this Software without specific prior written permission. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE SOFTWARE. */ ================================================ FILE: BGMApp/BGMXPCHelper/BGMXPCHelperService.h ================================================ // 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 . // // BGMXPCHelperService.h // BGMXPCHelper // // Copyright © 2016 Kyle Neideck // // Local Includes #import "BGMXPCProtocols.h" // System Includes #import // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is // 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection. @interface BGMXPCHelperService : NSObject - (id) initWithConnection:newConnection; @end ================================================ FILE: BGMApp/BGMXPCHelper/BGMXPCHelperService.mm ================================================ // 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 . // // BGMXPCHelperService.mm // BGMXPCHelper // // Copyright © 2016, 2017 Kyle Neideck // // Self Include #import "BGMXPCHelperService.h" // Local Includes #import "BGM_Utils.h" #import "BGMXPCListenerDelegate.h" #import "BGMBackgroundMusicDevice.h" // PublicUtility Includes #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; } + (NSError*) errorWithCode:(NSInteger)code description:(NSString*)description { return [NSError errorWithDomain:kBGMXPCHelperMachServiceName code:code userInfo:@{ NSLocalizedDescriptionKey: description }]; } + (NSError*) errorWithCode:(NSInteger)code description:(NSString*)description underlyingError:(NSError*)underlyingError { return [NSError errorWithDomain:kBGMXPCHelperMachServiceName code:code userInfo:@{ NSLocalizedDescriptionKey: description, NSUnderlyingErrorKey: underlyingError }]; } + (void) withBGMAppRemoteProxy:(void (^)(id))block errorHandler:(void (^)(NSError* error))errorHandler { // Retry by default return [BGMXPCHelperService withBGMAppRemoteProxy:block errorHandler:errorHandler retryOnError:YES]; } + (void) withBGMAppRemoteProxy:(void (^)(id))block errorHandler:(void (^)(NSError* error))errorHandler retryOnError:(BOOL)retry { // Wraps some error handling around a block that calls BGMApp remotely, and runs it. if (!sBGMAppConnection && sBGMAppEndpoint) { // Create a new connection to BGMApp from the endpoint @synchronized(self) { sBGMAppConnection = [[NSXPCConnection alloc] initWithListenerEndpoint:(NSXPCListenerEndpoint* __nonnull)sBGMAppEndpoint]; NSAssert(sBGMAppConnection, @"NSXPCConnection::initWithListenerEndpoint returned nil"); [sBGMAppConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(BGMAppXPCProtocol)]]; sBGMAppConnection.invalidationHandler = ^{ sBGMAppConnection = nil; }; [sBGMAppConnection resume]; } } if (sBGMAppConnection) { id proxy = [sBGMAppConnection remoteObjectProxyWithErrorHandler:^(NSError* error) { if (retry) { DebugMsg("BGMXPCHelperService::withBGMAppRemoteProxy: %s error=<%lu, %s>", "Error sending message to BGMApp. Creating new connection and retrying.", [error code], [[error localizedDescription] UTF8String]); // Clear the stored connection so a new one will be created when we retry. (The connection might still be // valid, but it's simpler to just make a new one every time.) @synchronized(self) { sBGMAppConnection = nil; } // Retry the message. [BGMXPCHelperService withBGMAppRemoteProxy:block errorHandler:errorHandler retryOnError:NO]; } else { NSLog(@"BGMXPCHelperService::withBGMAppRemoteProxy: Error sending message to BGMApp: %@", error); errorHandler(error); } }]; block(proxy); } else { errorHandler([BGMXPCHelperService errorWithCode:kBGMXPC_MessageFailure description:@"No connection to BGMApp"]); } } // 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 { [self debugWarnIfCalledByBGMDriver]; DebugMsg("BGMXPCHelperService::registerAsBGMAppWithListenerEndpoint: Received BGMApp listener endpoint"); // Store the connection (which we now know is from BGMApp) and endpoint so all instances of this class can use them. @synchronized([self class]) { sBGMAppEndpoint = endpoint; sBGMAppConnection = connection; [sBGMAppConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(BGMAppXPCProtocol)]]; // Set the stored connection back to nil when BGMApp closes, dies or invalidates the connection. sBGMAppConnection.interruptionHandler = ^{ @synchronized([self class]) { sBGMAppConnection = nil; } }; sBGMAppConnection.invalidationHandler = ^{ @synchronized([self class]) { sBGMAppConnection = nil; [self cleanUpForBGMApp]; } }; } reply(); } - (void) unregisterAsBGMApp { [self debugWarnIfCalledByBGMDriver]; DebugMsg("BGMXPCHelperService::unregisterAsBGMApp: Destroying connection to BGMApp"); // TODO: We don't want to assume only one instance of BGMApp will be running, in case multiple users are running it. @synchronized([self class]) { if (sBGMAppConnection) { [sBGMAppConnection invalidate]; sBGMAppConnection = nil; sBGMAppEndpoint = nil; } } } - (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 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). Note that ARC handles dispatch semaphores. dispatch_semaphore_t bgmAppReplySemaphore = dispatch_semaphore_create(0); DebugMsg("BGMXPCHelperService::startBGMAppPlayThroughSyncWithReply: Waiting for BGMApp to start IO on the output device"); // Send the message to BGMApp [BGMXPCHelperService withBGMAppRemoteProxy:^(id remoteObjectProxy) { [remoteObjectProxy startPlayThroughSyncWithReply:^(NSError* bgmAppReply) { replyToBGMDriver = bgmAppReply; dispatch_semaphore_signal(bgmAppReplySemaphore); } forUISoundsDevice:isUI]; } errorHandler:^(NSError* error) { replyToBGMDriver = [BGMXPCHelperService errorWithCode:kBGMXPC_MessageFailure description:[error localizedDescription] underlyingError:error]; dispatch_semaphore_signal(bgmAppReplySemaphore); }]; // Wait for BGMApp's reply long err = dispatch_semaphore_wait(bgmAppReplySemaphore, dispatch_time(DISPATCH_TIME_NOW, kStartIOTimeoutNsec)); if (err != 0) { replyToBGMDriver = [BGMXPCHelperService errorWithCode:kBGMXPC_Timeout description:@"Timed out waiting for BGMApp"]; } // Return the reply to BGMDriver 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 { #if DEBUG if ([connection effectiveUserIdentifier] != [BGMXPCListenerDelegate _coreaudiodUID]) { DebugMsg("BGMXPCHelperService::debugWarnIfCalledByBGMDriver: A method intended for BGMDriver only was (probably) called " "by BGMApp. Or it could have just been the tests."); NSLog(@"%@", [NSThread callStackSymbols]); } #endif } - (void) debugWarnIfCalledByBGMDriver { #if DEBUG if ([connection effectiveUserIdentifier] == [BGMXPCListenerDelegate _coreaudiodUID]) { DebugMsg("BGMXPCHelperService::debugWarnIfCalledByBGMDriver: A method intended for BGMApp only was (probably) called by BGMDriver"); NSLog(@"%@", [NSThread callStackSymbols]); } #endif } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMXPCHelper/BGMXPCListenerDelegate.h ================================================ // 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 . // // BGMXPCListenerDelegate.h // BGMXPCHelper // // Copyright © 2016 Kyle Neideck // // System Includes #import #pragma clang assume_nonnull begin @interface BGMXPCListenerDelegate : NSObject // The UID of the _coreaudiod user, which BGMDriver runs under. This is used in debug builds, usually to warn if a remote method is // called by BGMApp when it's only meant to be called by BGMDriver, or vice versa. + (uid_t) _coreaudiodUID; - (BOOL) listener:(NSXPCListener*)listener shouldAcceptNewConnection:(NSXPCConnection*)newConnection; @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMXPCHelper/BGMXPCListenerDelegate.m ================================================ // 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 . // // BGMXPCListenerDelegate.m // BGMXPCHelper // // Copyright © 2016 Kyle Neideck // // Self Include #import "BGMXPCListenerDelegate.h" // Local Includes #import "BGMXPCHelperService.h" // PublicUtility Includes #include "CADebugMacros.h" // System Includes #import #pragma clang assume_nonnull begin static uid_t sCoreaudiodUID = 0; // Uses 0 for unset @implementation BGMXPCListenerDelegate + (uid_t) _coreaudiodUID { // Lazily get the UID of the _coreaudiod user. if (sCoreaudiodUID == 0) { long bufferSize = sysconf(_SC_GETPW_R_SIZE_MAX); if (-1 == bufferSize) { @throw @"BGMXPCListenerDelegate::_coreaudiodUID: sysconf failed"; } char unusedBuffer[bufferSize]; struct passwd passwd, *resultPtr = NULL; if (0 != getpwnam_r("_coreaudiod", &passwd, unusedBuffer, bufferSize, &resultPtr)) { @throw @"BGMXPCListenerDelegate::_coreaudiodUID: getpwnam_r failed"; } // When getpwnam_r can't find the user it leaves resultPtr null if (resultPtr) { DebugMsg("BGMXPCListenerDelegate::_coreaudiodUID: Found user: name=%s uid=%u", passwd.pw_name, passwd.pw_uid); sCoreaudiodUID = passwd.pw_uid; } else { DebugMsg("BGMXPCListenerDelegate::_coreaudiodUID: Your system doesn't appear to have a user named \"_coreaudiod\". Unless " "there's something weird going on with your system, this is probably a bug."); sCoreaudiodUID = 0; } } return sCoreaudiodUID; } - (BOOL) listener:(NSXPCListener*)listener shouldAcceptNewConnection:(NSXPCConnection*)newConnection { #pragma unused (listener) // This method is where the NSXPCListener configures, accepts, and resumes a new incoming NSXPCConnection. DebugMsg("BGMXPCListenerDelegate::listener: Received new connection. effectiveUserIdentifier=%u _coreaudiodUID=%u", [newConnection effectiveUserIdentifier], [BGMXPCListenerDelegate _coreaudiodUID]); // Configure the connection. // First, set the interface that the exported object implements. newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BGMXPCHelperXPCProtocol)]; // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to // the exported object to handle. The connection retains the exported object. newConnection.exportedObject = [[BGMXPCHelperService alloc] initWithConnection:newConnection]; // Resuming the connection allows the system to deliver more incoming messages. [newConnection resume]; // Returning YES from this method tells the system that you have accepted this connection. If you want to reject the // connection for some reason, call -invalidate on the connection and return NO. return YES; } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMXPCHelper/Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleDisplayName BGMXPCHelper CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType XPC! CFBundleShortVersionString 0.4.3 CFBundleSignature ???? CFBundleVersion 1 NSHumanReadableCopyright Copyright © 2016-2024 Background Music contributors XPCService ServiceType User ================================================ FILE: BGMApp/BGMXPCHelper/com.bearisdriving.BGM.XPCHelper.plist.template ================================================ Label com.bearisdriving.BGM.XPCHelper AssociatedBundleIdentifiers com.bearisdriving.BGM.App ProgramArguments {{PATH_TO_BGMXPCHELPER}}/{{BGMXPCHELPER_EXECUTABLE_PATH}} MachServices com.bearisdriving.BGM.XPCHelper ProcessType Adaptive UserName {{BGMXPCHELPER_USER_NAME}} GroupName {{BGMXPCHELPER_GROUP_NAME}} ================================================ FILE: BGMApp/BGMXPCHelper/main.m ================================================ // 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 . // // main.m // BGMXPCHelper // // Copyright © 2016 Kyle Neideck // // BGMXPCHelper passes XPC messages between BGMDriver and BGMApp. So far it's only used for synchronization while starting IO. // // BGMApp and BGMDriver usually communicate by changing device properties and listening for notifications about those changes, which the // HAL sends. We use XPC, or plan to use it, for the few cases that notifications don't suit. For example, shared memory regions can be // sent via XPC. So we might be able to share a ring buffer between BGMDriver and BGMApp and avoid the latency and CPU overhead of // sending the audio data through an input stream. XPC should also let us safely send messages from real-time functions. // // Notifications would have probably worked fine for synchronizing BGMDriver and BGMApp while starting IO. But the Core Audio headers // don't specify which threads notifications are sent/received on, what (if anything) they can block on, or what can be blocked by their // handler callbacks. So using XPC instead makes it a bit easier to reason about the process and avoid potential deadlocks. (That said, // I ended up adding a huge amount of error handling and boilerplate code for XPC, so this way is probably more confusing overall.) // // CFMessagePort would be much simpler than XPC to use, but as far as I could tell there's no way to create a remote CFMessagePort for // a Mach port without looking the Mach port up using the bootstrap service. And because BGMXPCHelper and BGMApp have to use different // bootstrap namespaces, BGMXPCHelper wouldn't be able to see services vended by BGMApp. So BGMXPCHelper would be able to receive // messages from BGMApp and reply to them, but not send them. // // BGMXPCHelper has to run as a launchd daemon so the Mach service it vends will use the global bootstrap namespace. See Table 2 in // . This is because the coreaudiod process, where BGMDriver runs, // uses the global bootstrap namespace, which means it can only see services in that namespace. In practical terms, this just means we // have to run BGMXPCHelper by creating a launchd.plist file for it in /Library/LaunchDaemons and loading it with a command like // launchctl bootstrap system /Library/LaunchDaemons/com.bearisdriving.BGM.XPCHelper.plist // // Local Includes #import "BGMXPCProtocols.h" #import "BGMXPCListenerDelegate.h" // PublicUtility Includes #include "CADebugMacros.h" // System Includes #import #pragma clang assume_nonnull begin int main(int argc, const char* __nullable argv[]) { #pragma unused (argc, argv) DebugMsg("BGMXPCHelper::main: Service starting up"); // Set up the one NSXPCListener for this service. It will handle all incoming connections. This checks our service in with // the bootstrap service. NSXPCListener* listener = [[NSXPCListener alloc] initWithMachServiceName:kBGMXPCHelperMachServiceName]; BGMXPCListenerDelegate* delegate = [BGMXPCListenerDelegate new]; listener.delegate = delegate; // Start receiving requests. [listener resume]; [[NSRunLoop currentRunLoop] run]; return 0; } #pragma clang assume_nonnull end ================================================ FILE: BGMApp/BGMXPCHelper/post_install.sh ================================================ #!/bin/bash # vim: tw=100: # This file is part of Background Music. # # Background Music is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 2 of the # License, or (at your option) any later version. # # Background Music is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Background Music. If not, see . # # post_install.sh # BGMXPCHelper # # Copyright © 2016-2020 Kyle Neideck # # Installs BGMXPCHelper's launchd plist file and "bootstraps" (registers/enables) it with launchd. # # When you install BGMXPCHelper with xcodebuild (or Xcode itself, if that's possible) this script # runs as the final build phase. # PATH=/bin:/sbin:/usr/bin:/usr/sbin; export PATH # Check we have the paths we need, either in environment variables from Xcode or from the args. if [[ -z $1 ]]; then if [[ -z ${INSTALL_DIR} ]]; then echo "Environment variable INSTALL_DIR was not set." >&2 exit 1 fi else INSTALL_DIR="$1" fi if [[ -z $2 ]]; then if [[ -z ${EXECUTABLE_PATH} ]]; then echo "Environment variable EXECUTABLE_PATH was not set." >&2 exit 1 fi else EXECUTABLE_PATH="$2" fi if [[ -z $3 ]]; then if [[ -z ${TARGET_BUILD_DIR} ]]; then echo "Environment variable TARGET_BUILD_DIR was not set." >&2 exit 1 fi if [[ -z ${UNLOCALIZED_RESOURCES_FOLDER_PATH} ]]; then echo "Environment variable UNLOCALIZED_RESOURCES_FOLDER_PATH was not set." >&2 exit 1 fi RESOURCES_PATH="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" else RESOURCES_PATH="$3" fi # If DEPLOYMENT_POSTPROCESSING is true, xcodebuild calls this script even if you're just building # (and not also installing). I'm not sure why, as we have the "run script only when installing" # option enabled. # # REAL_ACTION is a workaround for xcodebuild setting ACTION to "install" even if we're actually # making an archive. # # TODO: Archiving BGMXPCHelper from Xcode instead of using build_and_install.sh still fails. if ( ! [[ -z ${ACTION} ]] && [[ "${ACTION}" != "install" ]] ) || \ ( ! [[ -z ${REAL_ACTION} ]] && [[ "${REAL_ACTION}" == "archive" ]] ); then echo "$0 should only be called during an install. Exiting." exit 0 fi # Safe mode. set -euo pipefail IFS=$'\n\t' # Show a warning if INSTALL_DIR isn't set to a safe installation directory. if [[ $(bash "${RESOURCES_PATH}/safe_install_dir.sh" "${INSTALL_DIR}") != 1 ]]; then echo "$(tput setaf 11)WARNING$(tput sgr0): Installing to \"${INSTALL_DIR}\" may be" \ "insecure. See safe_install_dir.sh for details." >&2 fi HELPER_USER=_BGMXPCHelper LAUNCHD_PLIST_INSTALL_PATH=/Library/LaunchDaemons LAUNCHD_PLIST_FILENAME=com.bearisdriving.BGM.XPCHelper.plist LAUNCHD_PLIST="${LAUNCHD_PLIST_INSTALL_PATH}/${LAUNCHD_PLIST_FILENAME}" # Create an unprivileged user for BGMXPCHelper to run as. if [[ "$(dscl . -search /Users RecordName ${HELPER_USER})" == "" ]]; then # Find an unused UID and GID. UIDs from 0 to 500 are reserved by OSX. HELPER_UID=$(dscl . -list /Users UniqueID | \ awk '{ a[$2] } END { for (i=501; i in a; i++); print i }') HELPER_GID=$(dscl . -list /Groups PrimaryGroupID | \ awk '{ a[$2] } END { for (i='${HELPER_UID}'; i in a; i++); print i }') # Check the UID and GID. NUMERIC_ID_REGEX='^[1-9][0-9]*$' ([[ "${HELPER_UID}" =~ ${NUMERIC_ID_REGEX} ]] && \ [[ ${HELPER_UID} -gt 500 ]] && \ [[ "$(dscl . -search /Users UniqueID ${HELPER_UID})" == "" ]]) || \ (echo "Internal error. Failed to generate a user ID. HELPER_UID=${HELPER_UID}" >&2; exit 1) ([[ "${HELPER_GID}" =~ ${NUMERIC_ID_REGEX} ]] && \ [[ ${HELPER_GID} -gt 500 ]] && \ [[ "$(dscl . -search /Groups PrimaryGroupID ${HELPER_GID})" == "" ]]) || \ (echo "Internal error. Failed to generate a group ID. HELPER_GID=${HELPER_GID}" >&2; exit 1) # Create the group. sudo dscl . -create /Groups/${HELPER_USER} PrimaryGroupID ${HELPER_GID} sudo dscl . -create /Groups/${HELPER_USER} RealName "Background Music XPC Helper Group" sudo dscl . -create /Groups/${HELPER_USER} Password '*' # Create the user. sudo dscl . -create /Users/${HELPER_USER} UniqueID ${HELPER_UID} sudo dscl . -create /Users/${HELPER_USER} PrimaryGroupID ${HELPER_GID} sudo dscl . -create /Users/${HELPER_USER} RealName "Background Music XPC Helper" sudo dscl . -create /Users/${HELPER_USER} Password '*' sudo dscl . -create /Users/${HELPER_USER} UserShell /usr/bin/false sudo dscl . -create /Users/${HELPER_USER} NFSHomeDirectory /var/empty sudo dscl . -delete /Users/${HELPER_USER} AuthenticationAuthority # Add the user to the group sudo dscl . -append /Groups/${HELPER_USER} GroupMembership ${HELPER_USER} # Check the user and group were created. [[ "$(dscl . -search /Users RecordName ${HELPER_USER})" != "" ]] || \ (echo "Internal error. Failed to ${HELPER_USER} user." >&2; exit 1) [[ "$(dscl . -search /Groups RecordName ${HELPER_USER})" != "" ]] || \ (echo "Internal error. Failed to ${HELPER_USER} group." >&2; exit 1) echo "Created ${HELPER_USER} user." fi # Copy the plist template into place. sudo cp "${RESOURCES_PATH}/${LAUNCHD_PLIST_FILENAME}.template" "${LAUNCHD_PLIST}" # Set the plist's owner and permissions. (Probably not necessary, but just in case.) sudo chown root:wheel "${LAUNCHD_PLIST}" sudo chmod 0644 "${LAUNCHD_PLIST}" # Replace the template variables in the plist. # First, escape the / characters in the values because we're using / in our sed pattern. The # solution usually recommended is to use | or # instead of /, but file and directory names can # contain those characters so I think this solution is safer. # # This is using Bash substring replacement to replace every occurrence of "/" with "\/". The general # form is ${string//substring/replacement}. (${string/substring/replacement} only replaces the first # match.) INSTALL_DIR_ESCAPED="${INSTALL_DIR//\//\\/}" EXECUTABLE_PATH_ESCAPED="${EXECUTABLE_PATH//\//\\/}" # Replace the variables in-place. They're formatted like "{{TEMPLATE_VARIABLE}}". sudo sed -i.tmp "s/{{PATH_TO_BGMXPCHELPER}}/${INSTALL_DIR_ESCAPED}/g" "${LAUNCHD_PLIST}" # EXECUTABLE_PATH is set by Xcode, currently to "BGMXPCHelper.xpc/Contents/MacOS/BGMXPCHelper". sudo sed -i.tmp "s/{{BGMXPCHELPER_EXECUTABLE_PATH}}/${EXECUTABLE_PATH_ESCAPED}/g" "${LAUNCHD_PLIST}" sudo sed -i.tmp "s/{{BGMXPCHELPER_USER_NAME}}/${HELPER_USER}/g" "${LAUNCHD_PLIST}" sudo sed -i.tmp "s/{{BGMXPCHELPER_GROUP_NAME}}/${HELPER_USER}/g" "${LAUNCHD_PLIST}" # Remove template-only comments. sudo sed -i.tmp 's/{{#.*#}}//g' "${LAUNCHD_PLIST}" # Clean up sed's temporary file. sudo rm -f "${LAUNCHD_PLIST}.tmp" echo "Installed ${LAUNCHD_PLIST_FILENAME} to ${LAUNCHD_PLIST_INSTALL_PATH}." # Unregister the plist. This disables the existing version of BGMXPCHelper. echo "Unregistering ${LAUNCHD_PLIST}. This will print errors if you don't already have a version" \ "of it registered, or your system uses an older version of launchctl. They're safe to ignore." echo "----" # The fallback versions of this command are for OS X 10.10 and 10.9, respectively. The "|| true" # part is so the command can fail, which it does if the plist isn't already installed, without -e # killing the script. sudo launchctl bootout system "${LAUNCHD_PLIST}" || \ sudo launchctl unbootstrap system "${LAUNCHD_PLIST}" || \ sudo launchctl unload "${LAUNCHD_PLIST}" || \ true echo "----" # Register the plist with launchd. This enables BGMXPCHelper. # The fallback version of this command is for OS X 10.9. sudo launchctl bootstrap system "${LAUNCHD_PLIST}" || \ sudo launchctl load "${LAUNCHD_PLIST}" echo "Started the BGMXPCHelper service." ================================================ FILE: BGMApp/BGMXPCHelper/safe_install_dir.sh ================================================ #!/bin/bash # vim: tw=100: # This file is part of Background Music. # # Background Music is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 2 of the # License, or (at your option) any later version. # # Background Music is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Background Music. If not, see . # # safe_install_dir.sh # BGMXPCHelper # # Copyright © 2016, 2017 Kyle Neideck # # Prints the path to a directory the BGMXPCHelper bundle can safely be installed to. Intended to be # used as the INSTALL_DIR environment variable for xcodebuild commands. For example, # xcodebuild -project BGMApp/BGMApp.xcodeproj -target BGMXPCHelper -configuration Debug \ # DSTROOT="/" INSTALL_DIR="$(BGMApp/BGMXPCHelper/safe_install_dir.sh)" install # # Instead of setting INSTALL_DIR, we could just move the installed bundle in post_install.sh, but # then we would have to leave an unused copy of the bundle in the default installation directory. If # we didn't, xcodebuild would fail in one of the post-processing steps it does after running # post_install.sh. # # The default installation directory comes from the Daemonomicon # , which recommends # installing daemons to /usr/local/libexec. But that isn't safe on many users' systems, because the # Daemonomicon also recommends that # # "[...] daemons be owned by root, have an owning group of wheel, and use permissions 755 # (rwxr-xr-x) for executables and directories, and 644 (rw-r--r--) for files. In addition, every # directory from your daemon up to the root directory must be owned by root and only writable by the # owner (or owned by root and sticky). If you don't do this correctly, a non-admin user might be # able to escalate their privileges by modifying your daemon (or shuffling it aside)." # # BGMXPCHelper runs as the unprivileged _BGMXPCHelper user, so I don't think it would be a security # risk right now, but I could be wrong about that. We could also forget about this and give the # _BGMXPCHelper privileges of some kind in a later version. # # Installing to /usr/local/libexec would be fine on a default OS X install, which doesn't have a # /usr/local directory, but if the user has Homebrew installed it will have set them as the owner of # /usr/local (even if it already existed and was owned by root). So if the owner or permissions for # /usr/local/libexec aren't what we want we try /Library/Application Support instead. # # If given a directory as an argument, this script will print "1" if the directory meets the # recommendation above, or "0" otherwise. # PATH=/bin:/sbin:/usr/bin:/usr/sbin; export PATH # Safe mode. set -euo pipefail IFS=$'\n\t' # Checks that a directory, and its parent directories, are owned by root and are only writable by # root. # # Takes one param, the directory to check. Sets DIR_IS_SAFE=1 if the directory is suitable. check_dir() { # Remember the current directory so we can come back here at the end of the function. pushd . > /dev/null # Normalize the path and follow symlinks. REAL_PATH=$(cd "$1" && pwd -P) cd "${REAL_PATH}" DIR_IS_SAFE=0 # While the current directory's owner has UID 0... while [[ "$(stat -f '%u' .)" == 0 ]] && \ # ...and isn't writable by the group or others... # (The stat command prints the current directory's permissions in octal, e.g. 755. We add a # leading 0 so Bash will interpret it as an octal value.) [[ $((0$(stat -f '%Lp' .) & 0022)) -eq 0 ]]; do # ...go upwards until we reach the root directory. cd .. if [[ "${PWD}" -ef / ]]; then DIR_IS_SAFE=1 break fi done # Go back to the directory we were in before this function was called. (No real reason to do # this yet, but it seemed like good practice.) popd > /dev/null } # Used when we can't find a suitable installation directory. Prints an error message and exits. # (Reaching this point should be very uncommon.) fail() { if [[ $ALLOW_UNSAFE_FALLBACK -eq 1 ]]; then CONTINUE_ANYWAY="y" else echo "$(tput setaf 11)WARNING$(tput sgr0): Installing BGMXPCHelper to its default" \ "location (${INSTALL_DIR} or, as a backup, ${BACKUP_INSTALL_DIR}) might not be" \ "secure on this system. It's recommended that each directory from the installation" \ "directory up to the root directory should be owned by root and not writable by any" \ "other user. See safe_install_dir.sh for more details." >&2 read -e -p "Continue anyway? [y/N]" CONTINUE_ANYWAY fi if [[ "${CONTINUE_ANYWAY}" == "y" ]] || [[ "${CONTINUE_ANYWAY}" == "Y" ]]; then echo "${INSTALL_DIR}" exit 0 else # Stops xcodebuild if the output is being used to set xcodebuild's INSTALL_DIR variable. echo "/dev/null" exit 1 fi } ALLOW_UNSAFE_FALLBACK=0 # This script can be given a directory to check as an argument, or the -y option, which tells this # script to print the default dir if neither of the dirs are safe. The pkg installer uses -y so it # can install anyway and show the user instructions to fix the permissions, rather than just # failing. # # (This line uses "${1+x}" instead of "$1" because having our "safe mode" enabled makes the script # fail if you reference an unset variable, even to check whether it's set or not.) if [[ ! -z "${1+x}" ]]; then if [[ "$1" == "-y" ]]; then ALLOW_UNSAFE_FALLBACK=1 else # Check the given path exists and is a directory. if [[ ! -d "$1" ]]; then echo "$1 is not a directory." >&2; exit 1; fi check_dir "$1" echo ${DIR_IS_SAFE} exit 0 fi fi # These are just for readability and to save keystrokes. If you change them, you'll have to change # other parts of the code as well. INSTALL_DIR="/usr/local/libexec" BACKUP_INSTALL_DIR="/Library/Application Support/Background Music" # Create the installation directory if it doesn't exist already. if [[ ! -e /usr/local ]]; then sudo mkdir /usr/local sudo chown root:wheel /usr/local sudo chmod go-w /usr/local fi if [[ ! -e "${INSTALL_DIR}" ]]; then sudo mkdir "${INSTALL_DIR}" sudo chown root:wheel "${INSTALL_DIR}" sudo chmod go-w "${INSTALL_DIR}" fi # Check the directory the build was installed to. check_dir "${INSTALL_DIR}" if [[ ${DIR_IS_SAFE} -eq 1 ]]; then FINAL_INSTALL_DIR="${INSTALL_DIR}" else # Try the backup directory instead. # Make sure /Library/Application Support exists. if [[ ! -e "/Library/Application Support" ]]; then fail; fi # Check /Library/Application Support before adding our directory. check_dir "/Library/Application Support" if [[ ${DIR_IS_SAFE} -ne 1 ]]; then fail; fi # Add a "Background Music" directory to /Library/Application Support. # (Check whether it exists first so we don't sudo unless we need to.) if [[ ! -e "${BACKUP_INSTALL_DIR}" ]]; then sudo mkdir "${BACKUP_INSTALL_DIR}" sudo chown root:wheel "${BACKUP_INSTALL_DIR}" sudo chmod go-w "${BACKUP_INSTALL_DIR}" fi # Check the backup directory just to be sure. check_dir "${BACKUP_INSTALL_DIR}" if [[ ${DIR_IS_SAFE} -ne 1 ]]; then fail; fi FINAL_INSTALL_DIR="${BACKUP_INSTALL_DIR}" fi echo "${FINAL_INSTALL_DIR}" ================================================ FILE: BGMApp/BGMXPCHelperTests/BGMXPCHelperTests-Info.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1 ================================================ FILE: BGMApp/BGMXPCHelperTests/BGMXPCHelperTests.m ================================================ // 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 . // // BGMXPCHelperTests.m // BGMXPCHelperTests // // Copyright © 2016 Kyle Neideck // // Local Includes #import "BGM_TestUtils.h" #import "BGMXPCProtocols.h" // System Includes #import #pragma clang assume_nonnull begin // To run these tests, BGMXPCHelper has to be installed and its launchd job enabled. @interface BGMXPCHelperTests : XCTestCase @end @implementation BGMXPCHelperTests { NSXPCConnection* connection; } - (void) setUp { [super setUp]; connection = [[NSXPCConnection alloc] initWithMachServiceName:kBGMXPCHelperMachServiceName options:NSXPCConnectionPrivileged]; connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(BGMXPCHelperXPCProtocol)]; [connection resume]; } - (void) tearDown { [connection invalidate]; [super tearDown]; } - (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] startBGMAppPlayThroughSyncWithReply:^(NSError* reply) { XCTAssertEqual([reply code], kBGMXPC_MessageFailure, @"Check that BGMApp isn't running, which would cause this failure"); dispatch_semaphore_signal(replySemaphore); } forUISoundsDevice:NO]; // Very long timeout to make it less likely to fail in CI builds 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"); } } @end #pragma clang assume_nonnull end ================================================ FILE: BGMApp/PublicUtility/BGMDebugLogging.c ================================================ // 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 . // // BGMDebugLogging.c // PublicUtility // // Copyright © 2020, 2024 Kyle Neideck // // Self Include #include "BGMDebugLogging.h" #pragma clang assume_nonnull begin // It's probably not ideal to use a global variable for this, but it's a lot easier. #if DEBUG || CoreAudio_Debug // Enable debug logging by default in debug builds. int gDebugLoggingIsEnabled = 1; #else int gDebugLoggingIsEnabled = 0; #endif // We don't bother synchronising accesses of gDebugLoggingIsEnabled because it isn't really // necessary and would complicate code that accesses it on realtime threads. int BGMDebugLoggingIsEnabled(void) { return gDebugLoggingIsEnabled; } void BGMSetDebugLoggingEnabled(int inEnabled) { gDebugLoggingIsEnabled = inEnabled; } #pragma clang assume_nonnull end ================================================ FILE: BGMApp/PublicUtility/BGMDebugLogging.h ================================================ // 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 . // // BGMDebugLogging.h // PublicUtility // // Copyright © 2020 Kyle Neideck // // Functions to globally enable/disable debug logging, i.e. more detailed logging to help diagnose // bugs. If debug logging is enabled, the DebugMsg macro from CADebugMacros.h (and possibly others) // will log messages. If not, it won't do anything. // // If the preprocessor macro CoreAudio_UseSysLog is true, which is currently the case for all build // variants (see BGMApp/BGMApp.xcodeproj/project.pbxproj and // BGMDriver/BGMDriver.xcodeproj/project.pbxproj), those messages will be logged using syslog and // can be read using Console.app. Try searching for "background music", "bgm" or "coreaudiod". // // Debug logging is enabled by default in debug builds, but in release builds you have to enable it // by option-clicking the status bar icon and then checking the Debug Logging menu item. Enabling // debug logging probably won't cause glitches, but we don't try to guarantee that and it's not // well tested. // #ifndef PublicUtility__BGMDebugLogging #define PublicUtility__BGMDebugLogging #pragma clang assume_nonnull begin /*! * @return Non-zero if debug logging is globally enabled. (Probably -- it's not synchronised.) * Real-time safe. */ #if defined(__cplusplus) extern "C" #endif int BGMDebugLoggingIsEnabled(void); /*! * @param inEnabled Non-zero to globally enable debug logging, zero to disable it. The change might * not be visible to other threads immediately. */ #if defined(__cplusplus) extern "C" #endif void BGMSetDebugLoggingEnabled(int inEnabled); #pragma clang assume_nonnull end #endif /* PublicUtility__BGMDebugLogging */ ================================================ FILE: BGMApp/PublicUtility/CAAtomic.h ================================================ /* File: CAAtomic.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ /* This file implements all Atomic operations using Interlocked functions specified in Winbase.h NOTE: According to Microsoft documentation, all Interlocked functions generates a full barrier. On Windows: As the Interlocked functions returns the Old value, Extra checks and operations are made after the atomic operation to return value consistent with OSX counterparts. */ #ifndef __CAAtomic_h__ #define __CAAtomic_h__ #if TARGET_OS_WIN32 #include #include #pragma intrinsic(_InterlockedOr) #pragma intrinsic(_InterlockedAnd) #else #include #include #endif #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" inline void CAMemoryBarrier() { #if TARGET_OS_WIN32 MemoryBarrier(); #else OSMemoryBarrier(); #endif } inline SInt32 CAAtomicAdd32Barrier(SInt32 theAmt, volatile SInt32* theValue) { #if TARGET_OS_WIN32 long lRetVal = InterlockedExchangeAdd((volatile long*)theValue, theAmt); // InterlockedExchangeAdd returns the original value which differs from OSX version. // At this point the addition would have occured and hence returning the new value // to keep it sync with OSX. return lRetVal + theAmt; #else return OSAtomicAdd32Barrier(theAmt, (volatile int32_t *)theValue); #endif } inline SInt32 CAAtomicOr32Barrier(UInt32 theMask, volatile UInt32* theValue) { #if TARGET_OS_WIN32 // InterlockedAnd macro is not defined in x86 platform, and hence using the intrinsic // function instead. long j = _InterlockedOr((volatile long*)theValue, theMask); // _InterlockedOr returns the original value which differs from OSX version. // Returning the new value similar to OSX return (SInt32)(j | theMask); #else return OSAtomicOr32Barrier(theMask, (volatile uint32_t *)theValue); #endif } inline SInt32 CAAtomicAnd32Barrier(UInt32 theMask, volatile UInt32* theValue) { #if TARGET_OS_WIN32 // InterlockedAnd macro is not defined in x86 platform, and hence using the intrinsic // function instead. long j = _InterlockedAnd((volatile long*)theValue, theMask); // _InterlockedAnd returns the original value which differs from OSX version. // Returning the new value similar to OSX return (SInt32)(j & theMask); #else return OSAtomicAnd32Barrier(theMask, (volatile uint32_t *)theValue); #endif } inline bool CAAtomicCompareAndSwap32Barrier(SInt32 oldValue, SInt32 newValue, volatile SInt32 *theValue) { #if TARGET_OS_WIN32 // InterlockedCompareExchange returns the old value. But we need to return bool value. long lRetVal = InterlockedCompareExchange((volatile long*)theValue, newValue, oldValue); // Hence we check if the new value is set and if it is we return true else false. // If theValue is equal to oldValue then the swap happens. Otherwise swap doesn't happen. return (oldValue == lRetVal); #else return OSAtomicCompareAndSwap32Barrier(oldValue, newValue, (volatile int32_t *)theValue); #endif } inline SInt32 CAAtomicIncrement32(volatile SInt32* theValue) { #if TARGET_OS_WIN32 return (SInt32)InterlockedIncrement((volatile long*)theValue); #else return OSAtomicIncrement32((volatile int32_t *)theValue); #endif } inline SInt32 CAAtomicDecrement32(volatile SInt32* theValue) { #if TARGET_OS_WIN32 return (SInt32)InterlockedDecrement((volatile long*)theValue); #else return OSAtomicDecrement32((volatile int32_t *)theValue); #endif } inline SInt32 CAAtomicIncrement32Barrier(volatile SInt32* theValue) { #if TARGET_OS_WIN32 return CAAtomicIncrement32(theValue); #else return OSAtomicIncrement32Barrier((volatile int32_t *)theValue); #endif } inline SInt32 CAAtomicDecrement32Barrier(volatile SInt32* theValue) { #if TARGET_OS_WIN32 return CAAtomicDecrement32(theValue); #else return OSAtomicDecrement32Barrier((volatile int32_t *)theValue); #endif } inline bool CAAtomicTestAndClearBarrier(int bitToClear, void* theAddress) { #if TARGET_OS_WIN32 BOOL bOldVal = InterlockedBitTestAndReset((long*)theAddress, bitToClear); return (bOldVal ? true : false); #else return OSAtomicTestAndClearBarrier(bitToClear, (volatile void *)theAddress); #endif } inline bool CAAtomicTestAndClear(int bitToClear, void* theAddress) { #if TARGET_OS_WIN32 BOOL bOldVal = CAAtomicTestAndClearBarrier(bitToClear, (long*)theAddress); return (bOldVal ? true : false); #else return OSAtomicTestAndClear(bitToClear, (volatile void *)theAddress); #endif } inline bool CAAtomicTestAndSetBarrier(int bitToSet, void* theAddress) { #if TARGET_OS_WIN32 BOOL bOldVal = InterlockedBitTestAndSet((long*)theAddress, bitToSet); return (bOldVal ? true : false); #else return OSAtomicTestAndSetBarrier(bitToSet, (volatile void *)theAddress); #endif } #pragma clang diagnostic pop // int32_t flavors -- for C++ only since we can't overload in C // CFBase.h defines SInt32 as signed int which is similar to int32_t. If CFBase.h is included, then // this will generate redefinition error. But on Mac, CFBase.h, still includes MacTypes.h where // SInt32 is defined as signed long so this would work there. // So in order to fix the redefinition errors, we define these functions only if MacTypes.h is included. #if defined(__cplusplus) && defined(__MACTYPES__) && !__LP64__ inline int32_t CAAtomicAdd32Barrier(int32_t theAmt, volatile int32_t* theValue) { return CAAtomicAdd32Barrier(theAmt, (volatile SInt32 *)theValue); } inline int32_t CAAtomicOr32Barrier(uint32_t theMask, volatile uint32_t* theValue) { return CAAtomicOr32Barrier(theMask, (volatile UInt32 *)theValue); } inline int32_t CAAtomicAnd32Barrier(uint32_t theMask, volatile uint32_t* theValue) { return CAAtomicAnd32Barrier(theMask, (volatile UInt32 *)theValue); } inline bool CAAtomicCompareAndSwap32Barrier(int32_t oldValue, int32_t newValue, volatile int32_t *theValue) { return CAAtomicCompareAndSwap32Barrier(oldValue, newValue, (volatile SInt32 *)theValue); } inline int32_t CAAtomicIncrement32(volatile int32_t* theValue) { return CAAtomicIncrement32((volatile SInt32 *)theValue); } inline int32_t CAAtomicDecrement32(volatile int32_t* theValue) { return CAAtomicDecrement32((volatile SInt32 *)theValue); } inline int32_t CAAtomicIncrement32Barrier(volatile int32_t* theValue) { return CAAtomicIncrement32Barrier((volatile SInt32 *)theValue); } inline int32_t CAAtomicDecrement32Barrier(volatile int32_t* theValue) { return CAAtomicDecrement32Barrier((volatile SInt32 *)theValue); } #endif // __cplusplus && !__LP64__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" #if __LP64__ inline bool CAAtomicCompareAndSwap64Barrier( int64_t __oldValue, int64_t __newValue, volatile int64_t *__theValue ) { return OSAtomicCompareAndSwap64Barrier(__oldValue, __newValue, __theValue); } #endif inline bool CAAtomicCompareAndSwapPtrBarrier(void *__oldValue, void *__newValue, volatile void ** __theValue) { #if __LP64__ return CAAtomicCompareAndSwap64Barrier((int64_t)__oldValue, (int64_t)__newValue, (int64_t *)__theValue); #else return CAAtomicCompareAndSwap32Barrier((int32_t)__oldValue, (int32_t)__newValue, (int32_t *)__theValue); #endif } #pragma clang diagnostic pop /* Spinlocks. These use memory barriers as required to synchronize access to shared * memory protected by the lock. The lock operation spins, but employs various strategies * to back off if the lock is held, making it immune to most priority-inversion livelocks. * The try operation immediately returns false if the lock was held, true if it took the * lock. The convention is that unlocked is zero, locked is nonzero. */ #define CA_SPINLOCK_INIT 0 typedef int32_t CASpinLock; bool CASpinLockTry( volatile CASpinLock *__lock ); void CASpinLockLock( volatile CASpinLock *__lock ); void CASpinLockUnlock( volatile CASpinLock *__lock ); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated" inline void CASpinLockLock( volatile CASpinLock *__lock ) { #if TARGET_OS_MAC OSSpinLockLock(__lock); #else while (CAAtomicTestAndSetBarrier(0, (void*)__lock)) usleep(1000); // ??? #endif } inline void CASpinLockUnlock( volatile CASpinLock *__lock ) { #if TARGET_OS_MAC OSSpinLockUnlock(__lock); #else CAAtomicTestAndClearBarrier(0, (void*)__lock); #endif } inline bool CASpinLockTry( volatile CASpinLock *__lock ) { #if TARGET_OS_MAC return OSSpinLockTry(__lock); #else return (CAAtomicTestAndSetBarrier(0, (void*)__lock) == 0); #endif } #pragma clang diagnostic pop #endif // __CAAtomic_h__ ================================================ FILE: BGMApp/PublicUtility/CAAutoDisposer.h ================================================ /* File: CAAutoDisposer.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAPtr_h__) #define __CAPtr_h__ #include // for malloc #include // for bad_alloc #include // for memset inline void* CA_malloc(size_t size) { void* p = malloc(size); if (!p && size) throw std::bad_alloc(); return p; } inline void* CA_realloc(void* old, size_t size) { #if TARGET_OS_WIN32 void* p = realloc(old, size); #else void* p = reallocf(old, size); // reallocf ensures the old pointer is freed if memory is full (p is NULL). #endif if (!p && size) throw std::bad_alloc(); return p; } #ifndef UINTPTR_MAX #if __LP64__ #define UINTPTR_MAX 18446744073709551615ULL #else #define UINTPTR_MAX 4294967295U #endif #endif inline void* CA_calloc(size_t n, size_t size) { // ensure that multiplication will not overflow if (n && UINTPTR_MAX / n < size) throw std::bad_alloc(); size_t nsize = n*size; void* p = malloc(nsize); if (!p && nsize) throw std::bad_alloc(); memset(p, 0, nsize); return p; } // helper class for automatic conversions template struct CAPtrRef { T* ptr_; explicit CAPtrRef(T* ptr) : ptr_(ptr) {} }; template class CAAutoFree { private: T* ptr_; public: CAAutoFree() : ptr_(0) {} explicit CAAutoFree(T* ptr) : ptr_(ptr) {} template CAAutoFree(CAAutoFree& that) : ptr_(that.release()) {} // take ownership // C++ std says: a template constructor is never a copy constructor CAAutoFree(CAAutoFree& that) : ptr_(that.release()) {} // take ownership CAAutoFree(size_t n, bool clear = false) // this becomes an ambiguous call if n == 0 : ptr_(0) { size_t maxItems = ~size_t(0) / sizeof(T); if (n > maxItems) throw std::bad_alloc(); ptr_ = static_cast(clear ? CA_calloc(n, sizeof(T)) : CA_malloc(n * sizeof(T))); } ~CAAutoFree() { free(); } void alloc(size_t numItems, bool clear = false) { size_t maxItems = ~size_t(0) / sizeof(T); if (numItems > maxItems) throw std::bad_alloc(); free(); ptr_ = static_cast(clear ? CA_calloc(numItems, sizeof(T)) : CA_malloc(numItems * sizeof(T))); } void allocBytes(size_t numBytes, bool clear = false) { free(); ptr_ = static_cast(clear ? CA_calloc(1, numBytes) : CA_malloc(numBytes)); } void reallocBytes(size_t numBytes) { ptr_ = static_cast(CA_realloc(ptr_, numBytes)); } void reallocItems(size_t numItems) { size_t maxItems = ~size_t(0) / sizeof(T); if (numItems > maxItems) throw std::bad_alloc(); ptr_ = static_cast(CA_realloc(ptr_, numItems * sizeof(T))); } template CAAutoFree& operator=(CAAutoFree& that) { set(that.release()); // take ownership return *this; } CAAutoFree& operator=(CAAutoFree& that) { set(that.release()); // take ownership return *this; } CAAutoFree& operator=(T* ptr) { set(ptr); return *this; } template CAAutoFree& operator=(U* ptr) { set(ptr); return *this; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } T* operator()() const { return ptr_; } T* get() const { return ptr_; } operator T*() const { return ptr_; } bool operator==(CAAutoFree const& that) const { return ptr_ == that.ptr_; } bool operator!=(CAAutoFree const& that) const { return ptr_ != that.ptr_; } bool operator==(T* ptr) const { return ptr_ == ptr; } bool operator!=(T* ptr) const { return ptr_ != ptr; } T* release() { // release ownership T* result = ptr_; ptr_ = 0; return result; } void set(T* ptr) { if (ptr != ptr_) { ::free(ptr_); ptr_ = ptr; } } void free() { set(0); } // automatic conversions to allow assignment from results of functions. // hard to explain. see auto_ptr implementation and/or Josuttis' STL book. CAAutoFree(CAPtrRef ref) : ptr_(ref.ptr_) { } CAAutoFree& operator=(CAPtrRef ref) { set(ref.ptr_); return *this; } template operator CAPtrRef() { return CAPtrRef(release()); } template operator CAAutoFree() { return CAAutoFree(release()); } }; template class CAAutoDelete { private: T* ptr_; public: CAAutoDelete() : ptr_(0) {} explicit CAAutoDelete(T* ptr) : ptr_(ptr) {} template CAAutoDelete(CAAutoDelete& that) : ptr_(that.release()) {} // take ownership // C++ std says: a template constructor is never a copy constructor CAAutoDelete(CAAutoDelete& that) : ptr_(that.release()) {} // take ownership ~CAAutoDelete() { free(); } template CAAutoDelete& operator=(CAAutoDelete& that) { set(that.release()); // take ownership return *this; } CAAutoDelete& operator=(CAAutoDelete& that) { set(that.release()); // take ownership return *this; } CAAutoDelete& operator=(T* ptr) { set(ptr); return *this; } template CAAutoDelete& operator=(U* ptr) { set(ptr); return *this; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } T* operator()() const { return ptr_; } T* get() const { return ptr_; } operator T*() const { return ptr_; } bool operator==(CAAutoDelete const& that) const { return ptr_ == that.ptr_; } bool operator!=(CAAutoDelete const& that) const { return ptr_ != that.ptr_; } bool operator==(T* ptr) const { return ptr_ == ptr; } bool operator!=(T* ptr) const { return ptr_ != ptr; } T* release() { // release ownership T* result = ptr_; ptr_ = 0; return result; } void set(T* ptr) { if (ptr != ptr_) { delete ptr_; ptr_ = ptr; } } void free() { set(0); } // automatic conversions to allow assignment from results of functions. // hard to explain. see auto_ptr implementation and/or Josuttis' STL book. CAAutoDelete(CAPtrRef ref) : ptr_(ref.ptr_) { } CAAutoDelete& operator=(CAPtrRef ref) { set(ref.ptr_); return *this; } template operator CAPtrRef() { return CAPtrRef(release()); } template operator CAAutoFree() { return CAAutoFree(release()); } }; template class CAAutoArrayDelete { private: T* ptr_; public: CAAutoArrayDelete() : ptr_(0) {} explicit CAAutoArrayDelete(T* ptr) : ptr_(ptr) {} template CAAutoArrayDelete(CAAutoArrayDelete& that) : ptr_(that.release()) {} // take ownership // C++ std says: a template constructor is never a copy constructor CAAutoArrayDelete(CAAutoArrayDelete& that) : ptr_(that.release()) {} // take ownership // this becomes an ambiguous call if n == 0 CAAutoArrayDelete(size_t n) : ptr_(new T[n]) {} ~CAAutoArrayDelete() { free(); } void alloc(size_t numItems) { free(); ptr_ = new T [numItems]; } template CAAutoArrayDelete& operator=(CAAutoArrayDelete& that) { set(that.release()); // take ownership return *this; } CAAutoArrayDelete& operator=(CAAutoArrayDelete& that) { set(that.release()); // take ownership return *this; } CAAutoArrayDelete& operator=(T* ptr) { set(ptr); return *this; } template CAAutoArrayDelete& operator=(U* ptr) { set(ptr); return *this; } T& operator*() const { return *ptr_; } T* operator->() const { return ptr_; } T* operator()() const { return ptr_; } T* get() const { return ptr_; } operator T*() const { return ptr_; } bool operator==(CAAutoArrayDelete const& that) const { return ptr_ == that.ptr_; } bool operator!=(CAAutoArrayDelete const& that) const { return ptr_ != that.ptr_; } bool operator==(T* ptr) const { return ptr_ == ptr; } bool operator!=(T* ptr) const { return ptr_ != ptr; } T* release() { // release ownership T* result = ptr_; ptr_ = 0; return result; } void set(T* ptr) { if (ptr != ptr_) { delete [] ptr_; ptr_ = ptr; } } void free() { set(0); } // automatic conversions to allow assignment from results of functions. // hard to explain. see auto_ptr implementation and/or Josuttis' STL book. CAAutoArrayDelete(CAPtrRef ref) : ptr_(ref.ptr_) { } CAAutoArrayDelete& operator=(CAPtrRef ref) { set(ref.ptr_); return *this; } template operator CAPtrRef() { return CAPtrRef(release()); } template operator CAAutoArrayDelete() { return CAAutoFree(release()); } }; // convenience function template void free(CAAutoFree& p) { p.free(); } //////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////// #if 0 // example program showing ownership transfer CAAutoFree source() { // source allocates and returns ownership to the caller. const char* str = "this is a test"; size_t size = strlen(str) + 1; CAAutoFree captr(size, false); strlcpy(captr(), str, size); printf("source %08X %08X '%s'\n", &captr, captr(), captr()); return captr; } void user(CAAutoFree const& captr) { // passed by const reference. user can access the pointer but does not take ownership. printf("user: %08X %08X '%s'\n", &captr, captr(), captr()); } void sink(CAAutoFree captr) { // passed by value. sink takes ownership and frees the pointer on return. printf("sink: %08X %08X '%s'\n", &captr, captr(), captr()); } int main (int argc, char * const argv[]) { CAAutoFree captr(source()); printf("main captr A %08X %08X\n", &captr, captr()); user(captr); sink(captr); printf("main captr B %08X %08X\n", &captr, captr()); return 0; } #endif #endif ================================================ FILE: BGMApp/PublicUtility/CABitOperations.h ================================================ /* File: CABitOperations.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #ifndef _CABitOperations_h_ #define _CABitOperations_h_ #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) //#include #include #else // #include #include "CFBase.h" #endif #include // return whether a number is a power of two inline UInt32 IsPowerOfTwo(UInt32 x) { return (x & (x-1)) == 0; } // count the leading zeros in a word // Metrowerks Codewarrior. powerpc native count leading zeros instruction: // I think it's safe to remove this ... //#define CountLeadingZeroes(x) ((int)__cntlzw((unsigned int)x)) inline UInt32 CountLeadingZeroes(UInt32 arg) { // GNUC / LLVM have a builtin #if defined(__GNUC__) || defined(__llvm___) #if (TARGET_CPU_X86 || TARGET_CPU_X86_64) if (arg == 0) return 32; #endif // TARGET_CPU_X86 || TARGET_CPU_X86_64 return __builtin_clz(arg); #elif TARGET_OS_WIN32 UInt32 tmp; __asm{ bsr eax, arg mov ecx, 63 cmovz eax, ecx xor eax, 31 mov tmp, eax // this moves the result in tmp to return. } return tmp; #else #error "Unsupported architecture" #endif // defined(__GNUC__) } // Alias (with different spelling) #define CountLeadingZeros CountLeadingZeroes inline UInt32 CountLeadingZeroesLong(UInt64 arg) { // GNUC / LLVM have a builtin #if defined(__GNUC__) || defined(__llvm___) #if (TARGET_CPU_X86 || TARGET_CPU_X86_64) if (arg == 0) return 64; #endif // TARGET_CPU_X86 || TARGET_CPU_X86_64 return __builtin_clzll(arg); #elif TARGET_OS_WIN32 UInt32 x = CountLeadingZeroes((UInt32)(arg >> 32)); if(x < 32) return x; else return 32+CountLeadingZeroes((UInt32)arg); #else #error "Unsupported architecture" #endif // defined(__GNUC__) } #define CountLeadingZerosLong CountLeadingZeroesLong // count trailing zeroes inline UInt32 CountTrailingZeroes(UInt32 x) { return 32 - CountLeadingZeroes(~x & (x-1)); } // count leading ones inline UInt32 CountLeadingOnes(UInt32 x) { return CountLeadingZeroes(~x); } // count trailing ones inline UInt32 CountTrailingOnes(UInt32 x) { return 32 - CountLeadingZeroes(x & (~x-1)); } // number of bits required to represent x. inline UInt32 NumBits(UInt32 x) { return 32 - CountLeadingZeroes(x); } // base 2 log of next power of two greater or equal to x inline UInt32 Log2Ceil(UInt32 x) { return 32 - CountLeadingZeroes(x - 1); } // base 2 log of next power of two less or equal to x inline UInt32 Log2Floor(UInt32 x) { return 32 - CountLeadingZeroes(x) - 1; } // next power of two greater or equal to x inline UInt32 NextPowerOfTwo(UInt32 x) { return 1 << Log2Ceil(x); } // counting the one bits in a word inline UInt32 CountOnes(UInt32 x) { // secret magic algorithm for counting bits in a word. x = x - ((x >> 1) & 0x55555555); x = (x & 0x33333333) + ((x >> 2) & 0x33333333); return (((x + (x >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24; } // counting the zero bits in a word inline UInt32 CountZeroes(UInt32 x) { return CountOnes(~x); } // return the bit position (0..31) of the least significant bit inline UInt32 LSBitPos(UInt32 x) { return CountTrailingZeroes(x & -(SInt32)x); } // isolate the least significant bit inline UInt32 LSBit(UInt32 x) { return x & -(SInt32)x; } // return the bit position (0..31) of the most significant bit inline UInt32 MSBitPos(UInt32 x) { return 31 - CountLeadingZeroes(x); } // isolate the most significant bit inline UInt32 MSBit(UInt32 x) { return 1 << MSBitPos(x); } // Division optimized for power of 2 denominators inline UInt32 DivInt(UInt32 numerator, UInt32 denominator) { if(IsPowerOfTwo(denominator)) return numerator >> (31 - CountLeadingZeroes(denominator)); else return numerator/denominator; } #endif ================================================ FILE: BGMApp/PublicUtility/CACFArray.cpp ================================================ /* File: CACFArray.cpp Abstract: CACFArray.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= // Self Include #include "CACFArray.h" // PublicUtility Includes #include "CACFDictionary.h" #include "CACFNumber.h" #include "CACFString.h" //============================================================================= // CACFArray //============================================================================= bool CACFArray::HasItem(const void* inItem) const { bool theAnswer = false; if(mCFArray != NULL) { CFRange theRange = { 0, CFArrayGetCount(mCFArray)}; theAnswer = CFArrayContainsValue(mCFArray, theRange, inItem); } return theAnswer; } bool CACFArray::GetIndexOfItem(const void* inItem, UInt32& outIndex) const { bool theAnswer = false; outIndex = 0; if(mCFArray != NULL) { CFRange theRange = { 0, CFArrayGetCount(mCFArray)}; CFIndex theIndex = CFArrayGetFirstIndexOfValue(mCFArray, theRange, inItem); if(theIndex != -1) { theAnswer = true; outIndex = ToUInt32(theIndex); } } return theAnswer; } bool CACFArray::GetBool(UInt32 inIndex, bool& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inIndex, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFBooleanGetTypeID())) { outValue = CFBooleanGetValue(static_cast(theValue)); theAnswer = true; } else if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { SInt32 theNumericValue = 0; CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &theNumericValue); outValue = theNumericValue != 0; theAnswer = true; } } return theAnswer; } bool CACFArray::GetSInt32(UInt32 inIndex, SInt32& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theItem), kCFNumberSInt32Type, &outItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetUInt32(UInt32 inIndex, UInt32& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theItem), kCFNumberSInt32Type, &outItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetSInt64(UInt32 inIndex, SInt64& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theItem), kCFNumberSInt64Type, &outItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetUInt64(UInt32 inIndex, UInt64& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theItem), kCFNumberSInt64Type, &outItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetFloat32(UInt32 inIndex, Float32& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theItem), kCFNumberFloat32Type, &outItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetFloat64(UInt32 inIndex, Float64& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theItem), kCFNumberFloat64Type, &outItem); theAnswer = true; } } return theAnswer; } bool CACFArray::Get4CC(UInt32 inIndex, UInt32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inIndex, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &outValue); theAnswer = true; } else if((theValue != NULL) && (CFGetTypeID(theValue) == CFStringGetTypeID())) { CFStringRef theString = static_cast(theValue); if(CFStringGetLength(theString) == 4) { char theCString[5]; CFStringGetCString(theString, theCString, 5, kCFStringEncodingASCII); outValue = CFSwapInt32BigToHost(*reinterpret_cast(theCString)); } } } return theAnswer; } bool CACFArray::GetString(UInt32 inIndex, CFStringRef& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFStringGetTypeID())) { outItem = static_cast(theItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetArray(UInt32 inIndex, CFArrayRef& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFArrayGetTypeID())) { outItem = static_cast(theItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetDictionary(UInt32 inIndex, CFDictionaryRef& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFDictionaryGetTypeID())) { outItem = static_cast(theItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetData(UInt32 inIndex, CFDataRef& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFDataGetTypeID())) { outItem = static_cast(theItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetUUID(UInt32 inIndex, CFUUIDRef& outItem) const { bool theAnswer = false; CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFUUIDGetTypeID())) { outItem = static_cast(theItem); theAnswer = true; } } return theAnswer; } bool CACFArray::GetCFType(UInt32 inIndex, CFTypeRef& outItem) const { bool theAnswer = false; if((mCFArray != NULL) && (inIndex < GetNumberItems())) { outItem = CFArrayGetValueAtIndex(mCFArray, static_cast(inIndex)); theAnswer = outItem != NULL; } return theAnswer; } void CACFArray::GetCACFString(UInt32 inIndex, CACFString& outItem) const { outItem = static_cast(NULL); CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFStringGetTypeID())) { outItem = static_cast(theItem); } } } void CACFArray::GetCACFArray(UInt32 inIndex, CACFArray& outItem) const { outItem = static_cast(NULL); CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFArrayGetTypeID())) { outItem = static_cast(theItem); } } } void CACFArray::GetCACFDictionary(UInt32 inIndex, CACFDictionary& outItem) const { outItem = static_cast(NULL); CFTypeRef theItem = NULL; if(GetCFType(inIndex, theItem)) { if((theItem != NULL) && (CFGetTypeID(theItem) == CFDictionaryGetTypeID())) { outItem = static_cast(theItem); } } } bool CACFArray::AppendBool(bool inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFBoolean theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFBoolean()); } } return theAnswer; } bool CACFArray::AppendSInt32(SInt32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::AppendUInt32(UInt32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::AppendSInt64(SInt64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::AppendUInt64(UInt64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::AppendFloat32(Float32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::AppendFloat64(Float64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = AppendCFType(theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::AppendString(const CFStringRef inItem) { return AppendCFType(inItem); } bool CACFArray::AppendArray(const CFArrayRef inItem) { return AppendCFType(inItem); } bool CACFArray::AppendDictionary(const CFDictionaryRef inItem) { return AppendCFType(inItem); } bool CACFArray::AppendData(const CFDataRef inItem) { return AppendCFType(inItem); } bool CACFArray::AppendCFType(const CFTypeRef inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CFArrayAppendValue(mCFArray, inItem); theAnswer = true; } return theAnswer; } bool CACFArray::InsertBool(UInt32 inIndex, bool inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFBoolean theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFBoolean()); } } return theAnswer; } bool CACFArray::InsertSInt32(UInt32 inIndex, SInt32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::InsertUInt32(UInt32 inIndex, UInt32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::InsertSInt64(UInt32 inIndex, SInt64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::InsertUInt64(UInt32 inIndex, UInt64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::InsertFloat32(UInt32 inIndex, Float32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::InsertFloat64(UInt32 inIndex, Float64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = InsertCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::InsertString(UInt32 inIndex, const CFStringRef inItem) { return InsertCFType(inIndex, inItem); } bool CACFArray::InsertArray(UInt32 inIndex, const CFArrayRef inItem) { return InsertCFType(inIndex, inItem); } bool CACFArray::InsertDictionary(UInt32 inIndex, const CFDictionaryRef inItem) { return InsertCFType(inIndex, inItem); } bool CACFArray::InsertData(UInt32 inIndex, const CFDataRef inItem) { return InsertCFType(inIndex, inItem); } bool CACFArray::InsertCFType(UInt32 inIndex, const CFTypeRef inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable) { if(inIndex < GetNumberItems()) { CFArrayInsertValueAtIndex(mCFArray, static_cast(inIndex), inItem); } else { CFArrayAppendValue(mCFArray, inItem); } theAnswer = true; } return theAnswer; } bool CACFArray::SetBool(UInt32 inIndex, bool inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFBoolean theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFBoolean()); } } return theAnswer; } bool CACFArray::SetSInt32(UInt32 inIndex, SInt32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::SetUInt32(UInt32 inIndex, UInt32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::SetSInt64(UInt32 inIndex, SInt64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::SetUInt64(UInt32 inIndex, UInt64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::SetFloat32(UInt32 inIndex, Float32 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::SetFloat64(UInt32 inIndex, Float64 inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CACFNumber theItem(inItem); if(theItem.IsValid()) { theAnswer = SetCFType(inIndex, theItem.GetCFNumber()); } } return theAnswer; } bool CACFArray::SetString(UInt32 inIndex, const CFStringRef inItem) { return SetCFType(inIndex, inItem); } bool CACFArray::SetArray(UInt32 inIndex, const CFArrayRef inItem) { return SetCFType(inIndex, inItem); } bool CACFArray::SetDictionary(UInt32 inIndex, const CFDictionaryRef inItem) { return SetCFType(inIndex, inItem); } bool CACFArray::SetData(UInt32 inIndex, const CFDataRef inItem) { return SetCFType(inIndex, inItem); } bool CACFArray::SetCFType(UInt32 inIndex, const CFTypeRef inItem) { bool theAnswer = false; if((mCFArray != NULL) && mMutable && (inIndex <= GetNumberItems())) { CFArraySetValueAtIndex(mCFArray, static_cast(inIndex), inItem); theAnswer = true; } return theAnswer; } ================================================ FILE: BGMApp/PublicUtility/CACFArray.h ================================================ /* File: CACFArray.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CACFArray_h__) #define __CACFArray_h__ //============================================================================= // Includes //============================================================================= // System Includes #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #include #else #include #include #endif #include "CADebugMacros.h" //============================================================================= // Types //============================================================================= class CACFDictionary; class CACFString; //============================================================================= // CACFArray //============================================================================= class CACFArray { // Construction/Destruction public: CACFArray() : mCFArray(CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks)), mRelease(true), mMutable(true) {} explicit CACFArray(bool inRelease) : mCFArray(CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks)), mRelease(inRelease), mMutable(true) {} CACFArray(UInt32 inMaxNumberItems, bool inRelease) : mCFArray(CFArrayCreateMutable(NULL, static_cast(inMaxNumberItems), &kCFTypeArrayCallBacks)), mRelease(inRelease), mMutable(true) {} CACFArray(CFArrayRef inCFArray, bool inRelease) : mCFArray(const_cast(inCFArray)), mRelease(inRelease), mMutable(false) {} CACFArray(CFMutableArrayRef inCFArray, bool inRelease) : mCFArray(inCFArray), mRelease(inRelease), mMutable(true) {} CACFArray(const CACFArray& inArray) : mCFArray(inArray.mCFArray), mRelease(inArray.mRelease), mMutable(inArray.mMutable) { Retain(); } CACFArray& operator=(const CACFArray& inArray) { Release(); mCFArray = inArray.mCFArray; mRelease = inArray.mRelease; mMutable = inArray.mMutable; Retain(); return *this; } CACFArray& operator=(CFArrayRef inCFArray) { Release(); mCFArray = const_cast(inCFArray); mMutable = false; Retain(); return *this; } CACFArray& operator=(CFMutableArrayRef inCFArray) { Release(); mCFArray = inCFArray; mMutable = true; Retain(); return *this; } ~CACFArray() { Release(); } private: void Retain() { if(mRelease && (mCFArray != NULL)) { CFRetain(mCFArray); } } void Release() { if(mRelease && (mCFArray != NULL)) { CFRelease(mCFArray); } } // Attributes public: bool IsValid() const { return mCFArray != NULL; } bool IsMutable() const { return mMutable; } bool CanModify() const { return mMutable && (mCFArray != NULL); } bool WillRelease() const { return mRelease; } void ShouldRelease(bool inRelease) { mRelease = inRelease; } CFTypeID GetTypeID() const { return CFGetTypeID(mCFArray); } CFArrayRef GetCFArray() const { return mCFArray; } CFArrayRef CopyCFArray() const { if(mCFArray != NULL) { CFRetain(mCFArray); } return mCFArray; } CFMutableArrayRef GetCFMutableArray() const { return mCFArray; } CFMutableArrayRef CopyCFMutableArray() const { if(mCFArray != NULL) { CFRetain(mCFArray); } return mCFArray; } CFPropertyListRef AsPropertyList() const { return mCFArray; } void SetCFMutableArrayFromCopy(CFArrayRef inArray, bool inRelease = true) { Release(); mCFArray = CFArrayCreateMutableCopy(NULL, 0, inArray); mMutable = true; mRelease = inRelease; } // Item Operations public: UInt32 GetNumberItems() const { UInt32 theAnswer = 0; if(mCFArray != NULL) { theAnswer = ToUInt32(CFArrayGetCount(mCFArray)); } return theAnswer; } bool HasItem(const void* inItem) const; void RemoveItem(const void* inItem) { UInt32 theIndex; if(CanModify() && GetIndexOfItem(inItem, theIndex)) { RemoveItemAtIndex(theIndex); } } bool GetIndexOfItem(const void* inItem, UInt32& outIndex) const; void RemoveItemAtIndex(UInt32 inIndex) { if(CanModify()) { CFArrayRemoveValueAtIndex(mCFArray, static_cast(inIndex)); } } void Clear() { if(CanModify()) { CFArrayRemoveAllValues(mCFArray); } } void Sort(CFComparatorFunction inCompareFunction) { if(CanModify()) { CFRange theRange = { 0, CFArrayGetCount(mCFArray) }; CFArraySortValues(mCFArray, theRange, inCompareFunction, NULL); } } void SortNumbers() { Sort((CFComparatorFunction)CFNumberCompare); } void SortStrings() { Sort((CFComparatorFunction)CFStringCompare); } bool GetBool(UInt32 inIndex, bool& outValue) const; bool GetSInt32(UInt32 inIndex, SInt32& outItem) const; bool GetUInt32(UInt32 inIndex, UInt32& outItem) const; bool GetSInt64(UInt32 inIndex, SInt64& outItem) const; bool GetUInt64(UInt32 inIndex, UInt64& outItem) const; bool GetFloat32(UInt32 inIndex, Float32& outItem) const; bool GetFloat64(UInt32 inIndex, Float64& outItem) const; bool Get4CC(UInt32 inIndex, UInt32& outValue) const; bool GetString(UInt32 inIndex, CFStringRef& outItem) const; bool GetArray(UInt32 inIndex, CFArrayRef& outItem) const; bool GetDictionary(UInt32 inIndex, CFDictionaryRef& outItem) const; bool GetData(UInt32 inIndex, CFDataRef& outItem) const; bool GetUUID(UInt32 inIndex, CFUUIDRef& outItem) const; bool GetCFType(UInt32 inIndex, CFTypeRef& outItem) const; void GetCACFString(UInt32 inIndex, CACFString& outItem) const; void GetCACFArray(UInt32 inIndex, CACFArray& outItem) const; void GetCACFDictionary(UInt32 inIndex, CACFDictionary& outItem) const; bool AppendBool(bool inItem); bool AppendSInt32(SInt32 inItem); bool AppendUInt32(UInt32 inItem); bool AppendSInt64(SInt64 inItem); bool AppendUInt64(UInt64 inItem); bool AppendFloat32(Float32 inItem); bool AppendFloat64(Float64 inItem); bool AppendString(const CFStringRef inItem); bool AppendArray(const CFArrayRef inItem); bool AppendDictionary(const CFDictionaryRef inItem); bool AppendData(const CFDataRef inItem); bool AppendCFType(const CFTypeRef inItem); bool InsertBool(UInt32 inIndex, bool inItem); bool InsertSInt32(UInt32 inIndex, SInt32 inItem); bool InsertUInt32(UInt32 inIndex, UInt32 inItem); bool InsertSInt64(UInt32 inIndex, SInt64 inItem); bool InsertUInt64(UInt32 inIndex, UInt64 inItem); bool InsertFloat32(UInt32 inIndex, Float32 inItem); bool InsertFloat64(UInt32 inIndex, Float64 inItem); bool InsertString(UInt32 inIndex, const CFStringRef inItem); bool InsertArray(UInt32 inIndex, const CFArrayRef inItem); bool InsertDictionary(UInt32 inIndex, const CFDictionaryRef inItem); bool InsertData(UInt32 inIndex, const CFDataRef inItem); bool InsertCFType(UInt32 inIndex, const CFTypeRef inItem); bool SetBool(UInt32 inIndex, bool inItem); bool SetSInt32(UInt32 inIndex, SInt32 inItem); bool SetUInt32(UInt32 inIndex, UInt32 inItem); bool SetSInt64(UInt32 inIndex, SInt64 inItem); bool SetUInt64(UInt32 inIndex, UInt64 inItem); bool SetFloat32(UInt32 inIndex, Float32 inItem); bool SetFloat64(UInt32 inIndex, Float64 inItem); bool SetString(UInt32 inIndex, const CFStringRef inItem); bool SetArray(UInt32 inIndex, const CFArrayRef inItem); bool SetDictionary(UInt32 inIndex, const CFDictionaryRef inItem); bool SetData(UInt32 inIndex, const CFDataRef inItem); bool SetCFType(UInt32 inIndex, const CFTypeRef inItem); // Implementation private: CFMutableArrayRef mCFArray; bool mRelease; bool mMutable; CACFArray(const void*); // prevent accidental instantiation with a pointer via bool constructor }; #endif ================================================ FILE: BGMApp/PublicUtility/CACFDictionary.cpp ================================================ /* File: CACFDictionary.cpp Abstract: CACFDictionary.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= // Self Include #include "CACFDictionary.h" // PublicUtility Includes #include "CACFArray.h" #include "CACFNumber.h" #include "CACFString.h" //============================================================================= // CACFDictionary //============================================================================= bool CACFDictionary::HasKey(const CFStringRef inKey) const { return CFDictionaryContainsKey(mCFDictionary, inKey) != 0; } UInt32 CACFDictionary::Size () const { return mCFDictionary ? ToUInt32(CFDictionaryGetCount(mCFDictionary)) : 0; } void CACFDictionary::GetKeys (const void **keys) const { CFDictionaryGetKeysAndValues(mCFDictionary, keys, NULL); } void CACFDictionary::GetKeysAndValues (const void **keys, const void **values) const { CFDictionaryGetKeysAndValues(mCFDictionary, keys, values); } bool CACFDictionary::GetBool(const CFStringRef inKey, bool& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFBooleanGetTypeID())) { outValue = CFBooleanGetValue(static_cast(theValue)); theAnswer = true; } else if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { SInt32 theNumericValue = 0; CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &theNumericValue); outValue = theNumericValue != 0; theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetSInt32(const CFStringRef inKey, SInt32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &outValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetUInt32(const CFStringRef inKey, UInt32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &outValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetSInt64(const CFStringRef inKey, SInt64& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberSInt64Type, &outValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetUInt64(const CFStringRef inKey, UInt64& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberSInt64Type, &outValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetFloat32FromString(const CFStringRef inKey, Float32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFStringGetTypeID())) { outValue = static_cast(CFStringGetDoubleValue(static_cast(theValue))); } } return theAnswer; } bool CACFDictionary::GetUInt32FromString(const CFStringRef inKey, UInt32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFStringGetTypeID())) { outValue = CFStringGetIntValue(static_cast(theValue)); } } return theAnswer; } bool CACFDictionary::GetFloat32(const CFStringRef inKey, Float32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberFloat32Type, &outValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetFloat64(const CFStringRef inKey, Float64& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberFloat64Type, &outValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetFixed32(const CFStringRef inKey, Float32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { SInt32 theFixed32 = 0; CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &theFixed32); // this is a 16.16 value so convert it to a float Float32 theSign = theFixed32 < 0 ? -1.0f : 1.0f; theFixed32 *= (SInt32)theSign; Float32 theWholePart = (theFixed32 & 0x7FFF0000) >> 16; Float32 theFractPart = theFixed32 & 0x0000FFFF; theFractPart /= 65536.0f; outValue = theSign * (theWholePart + theFractPart); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetFixed64(const CFStringRef inKey, Float64& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { SInt64 theFixed64 = 0; CFNumberGetValue(static_cast(theValue), kCFNumberSInt64Type, &theFixed64); outValue = static_cast(theFixed64 >> 32); outValue += static_cast(theFixed64 & 0x00000000FFFFFFFFLL) / static_cast(0x0000000100000000LL); theAnswer = true; } } return theAnswer; } bool CACFDictionary::Get4CC(const CFStringRef inKey, UInt32& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFNumberGetTypeID())) { CFNumberGetValue(static_cast(theValue), kCFNumberSInt32Type, &outValue); theAnswer = true; } else if((theValue != NULL) && (CFGetTypeID(theValue) == CFStringGetTypeID())) { CFStringRef theString = static_cast(theValue); if(CFStringGetLength(theString) == 4) { char theCString[5]; CFStringGetCString(theString, theCString, 5, kCFStringEncodingASCII); outValue = CFSwapInt32BigToHost(*reinterpret_cast(theCString)); } } } return theAnswer; } bool CACFDictionary::GetString(const CFStringRef inKey, CFStringRef& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFStringGetTypeID())) { outValue = static_cast(theValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetArray(const CFStringRef inKey, CFArrayRef& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFArrayGetTypeID())) { outValue = static_cast(theValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetDictionary(const CFStringRef inKey, CFDictionaryRef& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFDictionaryGetTypeID())) { outValue = static_cast(theValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetData(const CFStringRef inKey, CFDataRef& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFDataGetTypeID())) { outValue = static_cast(theValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetCFType(const CFStringRef inKey, CFTypeRef& outValue) const { bool theAnswer = false; if(mCFDictionary != NULL) { outValue = CFDictionaryGetValue(mCFDictionary, inKey); theAnswer = (outValue != NULL); } return theAnswer; } bool CACFDictionary::GetURL(const CFStringRef inKey, CFURLRef& outValue) const { bool theAnswer = false; CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFURLGetTypeID())) { outValue = static_cast(theValue); theAnswer = true; } } return theAnswer; } bool CACFDictionary::GetCFTypeWithCStringKey(const char* inKey, CFTypeRef& outValue) const { bool theAnswer = false; if(mCFDictionary != NULL) { CACFString theKey(inKey); if(theKey.IsValid()) { theAnswer = GetCFType(theKey.GetCFString(), outValue); } } return theAnswer; } void CACFDictionary::GetCACFString(const CFStringRef inKey, CACFString& outValue) const { outValue = static_cast(NULL); CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFStringGetTypeID())) { outValue = static_cast(theValue); } } } void CACFDictionary::GetCACFArray(const CFStringRef inKey, CACFArray& outValue) const { outValue = static_cast(NULL); CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFArrayGetTypeID())) { outValue = static_cast(theValue); } } } void CACFDictionary::GetCACFDictionary(const CFStringRef inKey, CACFDictionary& outValue) const { outValue = static_cast(NULL); CFTypeRef theValue = NULL; if(GetCFType(inKey, theValue)) { if((theValue != NULL) && (CFGetTypeID(theValue) == CFDictionaryGetTypeID())) { outValue = static_cast(theValue); } } } bool CACFDictionary::AddBool(const CFStringRef inKey, bool inValue) { CACFBoolean theValue(inValue); return AddCFType(inKey, theValue.GetCFBoolean()); } bool CACFDictionary::AddSInt32(const CFStringRef inKey, SInt32 inValue) { CACFNumber theValue(inValue); return AddCFType(inKey, theValue.GetCFNumber()); } bool CACFDictionary::AddUInt32(const CFStringRef inKey, UInt32 inValue) { CACFNumber theValue(inValue); return AddCFType(inKey, theValue.GetCFNumber()); } bool CACFDictionary::AddSInt64(const CFStringRef inKey, SInt64 inValue) { CACFNumber theValue(inValue); return AddCFType(inKey, theValue.GetCFNumber()); } bool CACFDictionary::AddUInt64(const CFStringRef inKey, UInt64 inValue) { CACFNumber theValue(inValue); return AddCFType(inKey, theValue.GetCFNumber()); } bool CACFDictionary::AddFloat32(const CFStringRef inKey, Float32 inValue) { CACFNumber theValue(inValue); return AddCFType(inKey, theValue.GetCFNumber()); } bool CACFDictionary::AddFloat64(const CFStringRef inKey, Float64 inValue) { CACFNumber theValue(inValue); return AddCFType(inKey, theValue.GetCFNumber()); } bool CACFDictionary::AddNumber(const CFStringRef inKey, const CFNumberRef inValue) { return AddCFType(inKey, inValue); } bool CACFDictionary::AddString(const CFStringRef inKey, const CFStringRef inValue) { return AddCFType(inKey, inValue); } bool CACFDictionary::AddArray(const CFStringRef inKey, const CFArrayRef inValue) { return AddCFType(inKey, inValue); } bool CACFDictionary::AddDictionary(const CFStringRef inKey, const CFDictionaryRef inValue) { return AddCFType(inKey, inValue); } bool CACFDictionary::AddData(const CFStringRef inKey, const CFDataRef inValue) { return AddCFType(inKey, inValue); } bool CACFDictionary::AddURL(const CFStringRef inKey, const CFURLRef inValue) { return AddCFType (inKey, inValue); } bool CACFDictionary::AddCFTypeWithCStringKey(const char* inKey, const CFTypeRef inValue) { bool theAnswer = false; if (inKey) { CACFString theKey(inKey); if(theKey.IsValid()) { theAnswer = AddCFType(theKey.GetCFString(), inValue); } } return theAnswer; } bool CACFDictionary::AddCString(const CFStringRef inKey, const char* inValue) { bool theAnswer = false; if (inValue) { CACFString theValue(inValue); if(theValue.IsValid()) { theAnswer = AddCFType(inKey, theValue.GetCFString()); } } return theAnswer; } bool CACFDictionary::AddCFType(const CFStringRef inKey, const CFTypeRef inValue) { bool theAnswer = false; if(mMutable && (mCFDictionary != NULL) && inValue) { CFDictionarySetValue(mCFDictionary, inKey, inValue); theAnswer = true; } return theAnswer; } ================================================ FILE: BGMApp/PublicUtility/CACFDictionary.h ================================================ /* File: CACFDictionary.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CACFDictionary_h__) #define __CACFDictionary_h__ //============================================================================= // Includes //============================================================================= // System Includes #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include #endif //============================================================================= // Types //============================================================================= class CACFArray; class CACFString; //============================================================================= // CACFDictionary //============================================================================= class CACFDictionary { // Construction/Destruction public: CACFDictionary() : mCFDictionary(CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)), mRelease(true), mMutable(true) {} explicit CACFDictionary(bool inRelease) : mCFDictionary(CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)), mRelease(inRelease), mMutable(true) {} CACFDictionary(CFDictionaryRef inCFDictionary, bool inRelease) : mCFDictionary(const_cast(inCFDictionary)), mRelease(inRelease), mMutable(false) {} CACFDictionary(CFMutableDictionaryRef inCFDictionary, bool inRelease) : mCFDictionary(inCFDictionary), mRelease(inRelease), mMutable(true) {} CACFDictionary(const CACFDictionary& inDictionary) : mCFDictionary(inDictionary.mCFDictionary), mRelease(inDictionary.mRelease), mMutable(inDictionary.mMutable) { Retain(); } CACFDictionary& operator=(const CACFDictionary& inDictionary) { Release(); mCFDictionary = inDictionary.mCFDictionary; mRelease = inDictionary.mRelease; mMutable = inDictionary.mMutable; Retain(); return *this; } CACFDictionary& operator=(CFDictionaryRef inDictionary) { Release(); mCFDictionary = const_cast(inDictionary); mMutable = false; Retain(); return *this; } CACFDictionary& operator=(CFMutableDictionaryRef inDictionary) { Release(); mCFDictionary = inDictionary; mMutable = true; Retain(); return *this; } ~CACFDictionary() { Release(); } private: void Retain() { if(mRelease && (mCFDictionary != NULL)) { CFRetain(mCFDictionary); } } void Release() { if(mRelease && (mCFDictionary != NULL)) { CFRelease(mCFDictionary); } } // Attributes public: bool IsValid() const { return mCFDictionary != NULL; } bool IsMutable() const { return mMutable;} bool CanModify() const { return mMutable && (mCFDictionary != NULL); } bool WillRelease() const { return mRelease; } void ShouldRelease(bool inRelease) { mRelease = inRelease; } CFDictionaryRef GetDict() const { return mCFDictionary; } CFDictionaryRef GetCFDictionary() const { return mCFDictionary; } CFDictionaryRef CopyCFDictionary() const { if(mCFDictionary != NULL) { CFRetain(mCFDictionary); } return mCFDictionary; } CFMutableDictionaryRef GetMutableDict() { return mCFDictionary; } CFMutableDictionaryRef GetCFMutableDictionary() const { return mCFDictionary; } CFMutableDictionaryRef CopyCFMutableDictionary() const { if(mCFDictionary != NULL) { CFRetain(mCFDictionary); } return mCFDictionary; } void SetCFMutableDictionaryFromCopy(CFDictionaryRef inDictionary, bool inRelease = true) { Release(); mCFDictionary = CFDictionaryCreateMutableCopy(NULL, 0, inDictionary); mMutable = true; mRelease = inRelease; } void SetCFMutableDictionaryToEmpty(bool inRelease = true) { Release(); mCFDictionary = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); mMutable = true; mRelease = inRelease; } CFPropertyListRef AsPropertyList() const { return mCFDictionary; } OSStatus GetDictIfMutable(CFMutableDictionaryRef& outDict) const { OSStatus theAnswer = -1; if(mMutable) { outDict = mCFDictionary; theAnswer = 0; } return theAnswer; } // Item Operations public: bool HasKey(const CFStringRef inKey) const; UInt32 Size() const; void GetKeys(const void** keys) const; void GetKeysAndValues (const void **keys, const void **values) const; bool GetBool(const CFStringRef inKey, bool& outValue) const; bool GetSInt32(const CFStringRef inKey, SInt32& outValue) const; bool GetUInt32(const CFStringRef inKey, UInt32& outValue) const; bool GetUInt32FromString(const CFStringRef inKey, UInt32& outValue) const; bool GetSInt64(const CFStringRef inKey, SInt64& outValue) const; bool GetUInt64(const CFStringRef inKey, UInt64& outValue) const; bool GetFloat32(const CFStringRef inKey, Float32& outValue) const; bool GetFloat32FromString(const CFStringRef inKey, Float32& outValue) const; bool GetFloat64(const CFStringRef inKey, Float64& outValue) const; bool GetFixed32(const CFStringRef inKey, Float32& outValue) const; bool GetFixed64(const CFStringRef inKey, Float64& outValue) const; bool Get4CC(const CFStringRef inKey, UInt32& outValue) const; bool GetString(const CFStringRef inKey, CFStringRef& outValue) const; bool GetArray(const CFStringRef inKey, CFArrayRef& outValue) const; bool GetDictionary(const CFStringRef inKey, CFDictionaryRef& outValue) const; bool GetData(const CFStringRef inKey, CFDataRef& outValue) const; bool GetCFType(const CFStringRef inKey, CFTypeRef& outValue) const; bool GetURL(const CFStringRef inKey, CFURLRef& outValue) const; bool GetCFTypeWithCStringKey(const char* inKey, CFTypeRef& outValue) const; void GetCACFString(const CFStringRef inKey, CACFString& outItem) const; void GetCACFArray(const CFStringRef inKey, CACFArray& outItem) const; void GetCACFDictionary(const CFStringRef inKey, CACFDictionary& outItem) const; bool AddBool(const CFStringRef inKey, bool inValue); bool AddSInt32(const CFStringRef inKey, SInt32 inValue); bool AddUInt32(const CFStringRef inKey, UInt32 inValue); bool AddSInt64(const CFStringRef inKey, SInt64 inValue); bool AddUInt64(const CFStringRef inKey, UInt64 inValue); bool AddFloat32(const CFStringRef inKey, Float32 inValue); bool AddFloat64(const CFStringRef inKey, Float64 inValue); bool AddNumber(const CFStringRef inKey, const CFNumberRef inValue); bool AddString(const CFStringRef inKey, const CFStringRef inValue); bool AddArray(const CFStringRef inKey, const CFArrayRef inValue); bool AddDictionary(const CFStringRef inKey, const CFDictionaryRef inValue); bool AddData(const CFStringRef inKey, const CFDataRef inValue); bool AddCFType(const CFStringRef inKey, const CFTypeRef inValue); bool AddURL(const CFStringRef inKey, const CFURLRef inValue); bool AddCFTypeWithCStringKey(const char* inKey, const CFTypeRef inValue); bool AddCString(const CFStringRef inKey, const char* inValue); void RemoveKey(const CFStringRef inKey) { if(CanModify()) { CFDictionaryRemoveValue(mCFDictionary, inKey); } } void Clear() { if(CanModify()) { CFDictionaryRemoveAllValues(mCFDictionary); } } void Show() { CFShow(mCFDictionary); } // Implementation private: CFMutableDictionaryRef mCFDictionary; bool mRelease; bool mMutable; CACFDictionary(const void*); // prevent accidental instantiation with a pointer via bool constructor }; #endif //__CACFDictionary_h__ ================================================ FILE: BGMApp/PublicUtility/CACFNumber.cpp ================================================ /* File: CACFNumber.cpp Abstract: CACFNumber.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= #include "CACFNumber.h" //============================================================================= // CACFNumber //============================================================================= Float32 CACFNumber::GetFixed32() const { SInt32 theFixedValue = GetSInt32(); // this is a 16.16 value so convert it to a float Float32 theSign = theFixedValue < 0 ? -1.0f : 1.0f; theFixedValue *= (SInt32)theSign; Float32 theWholePart = (theFixedValue & 0x7FFF0000) >> 16; Float32 theFractPart = theFixedValue & 0x0000FFFF; theFractPart /= 65536.0f; return theSign * (theWholePart + theFractPart); } Float64 CACFNumber::GetFixed64() const { SInt64 theFixedValue = GetSInt64(); // this is a 32.32 value so convert it to a double Float64 theSign = theFixedValue < 0 ? -1.0 : 1.0; theFixedValue *= (SInt64)theSign; Float64 theWholePart = (theFixedValue & 0x7FFFFFFF00000000LL) >> 32; Float64 theFractPart = theFixedValue & 0x00000000FFFFFFFFLL; theFractPart /= 4294967296.0; return theSign * (theWholePart + theFractPart); } ================================================ FILE: BGMApp/PublicUtility/CACFNumber.h ================================================ /* File: CACFNumber.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CACFNumber_h__) #define __CACFNumber_h__ //============================================================================= // Includes //============================================================================= #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #include #else #include #include #endif //============================================================================= // CACFBoolean //============================================================================= class CACFBoolean { // Construction/Destruction public: explicit CACFBoolean(CFBooleanRef inCFBoolean) : mCFBoolean(inCFBoolean), mWillRelease(true) {} CACFBoolean(CFBooleanRef inCFBoolean, bool inWillRelease) : mCFBoolean(inCFBoolean), mWillRelease(inWillRelease) {} explicit CACFBoolean(bool inValue) : mCFBoolean(inValue ? kCFBooleanTrue : kCFBooleanFalse), mWillRelease(true) { Retain(); } ~CACFBoolean() { Release(); } CACFBoolean(const CACFBoolean& inBoolean) : mCFBoolean(inBoolean.mCFBoolean), mWillRelease(inBoolean.mWillRelease) { Retain(); } CACFBoolean& operator=(const CACFBoolean& inBoolean) { Release(); mCFBoolean = inBoolean.mCFBoolean; mWillRelease = inBoolean.mWillRelease; Retain(); return *this; } CACFBoolean& operator=(CFBooleanRef inCFBoolean) { Release(); mCFBoolean = inCFBoolean; mWillRelease = true; return *this; } private: void Retain() { if(mWillRelease && (mCFBoolean != NULL)) { CFRetain(mCFBoolean); } } void Release() { if(mWillRelease && (mCFBoolean != NULL)) { CFRelease(mCFBoolean); } } CFBooleanRef mCFBoolean; bool mWillRelease; // Operations public: void AllowRelease() { mWillRelease = true; } void DontAllowRelease() { mWillRelease = false; } bool IsValid() { return mCFBoolean != NULL; } // Value Access public: CFBooleanRef GetCFBoolean() const { return mCFBoolean; } CFBooleanRef CopyCFBoolean() const { if(mCFBoolean != NULL) { CFRetain(mCFBoolean); } return mCFBoolean; } bool GetBoolean() const { bool theAnswer = false; if(mCFBoolean != NULL) { theAnswer = CFEqual(mCFBoolean, kCFBooleanTrue); } return theAnswer; } CACFBoolean(const void*); // prevent accidental instantiation with a pointer via bool constructor }; //============================================================================= // CACFNumber //============================================================================= class CACFNumber { // Construction/Destruction public: explicit CACFNumber(CFNumberRef inCFNumber) : mCFNumber(inCFNumber), mWillRelease(true) {} CACFNumber(CFNumberRef inCFNumber, bool inWillRelease) : mCFNumber(inCFNumber), mWillRelease(inWillRelease) {} CACFNumber(SInt32 inValue) : mCFNumber(CFNumberCreate(NULL, kCFNumberSInt32Type, &inValue)), mWillRelease(true) {} CACFNumber(UInt32 inValue) : mCFNumber(CFNumberCreate(NULL, kCFNumberSInt32Type, &inValue)), mWillRelease(true) {} CACFNumber(SInt64 inValue) : mCFNumber(CFNumberCreate(NULL, kCFNumberSInt64Type, &inValue)), mWillRelease(true) {} CACFNumber(UInt64 inValue) : mCFNumber(CFNumberCreate(NULL, kCFNumberSInt64Type, &inValue)), mWillRelease(true) {} CACFNumber(Float32 inValue) : mCFNumber(CFNumberCreate(NULL, kCFNumberFloat32Type, &inValue)), mWillRelease(true) {} CACFNumber(Float64 inValue) : mCFNumber(CFNumberCreate(NULL, kCFNumberFloat64Type, &inValue)), mWillRelease(true) {} ~CACFNumber() { Release(); } CACFNumber(const CACFNumber& inNumber) : mCFNumber(inNumber.mCFNumber), mWillRelease(inNumber.mWillRelease) { Retain(); } CACFNumber& operator=(const CACFNumber& inNumber) { Release(); mCFNumber = inNumber.mCFNumber; mWillRelease = inNumber.mWillRelease; Retain(); return *this; } CACFNumber& operator=(CFNumberRef inCFNumber) { Release(); mCFNumber = inCFNumber; mWillRelease = true; return *this; } private: void Retain() { if(mWillRelease && (mCFNumber != NULL)) { CFRetain(mCFNumber); } } void Release() { if(mWillRelease && (mCFNumber != NULL)) { CFRelease(mCFNumber); } } CFNumberRef mCFNumber; bool mWillRelease; // Operations public: void AllowRelease() { mWillRelease = true; } void DontAllowRelease() { mWillRelease = false; } bool IsValid() const { return mCFNumber != NULL; } // Value Access public: CFNumberRef GetCFNumber() const { return mCFNumber; } CFNumberRef CopyCFNumber() const { if(mCFNumber != NULL) { CFRetain(mCFNumber); } return mCFNumber; } SInt8 GetSInt8() const { SInt8 theAnswer = 0; if(mCFNumber != NULL) { CFNumberGetValue(mCFNumber, kCFNumberSInt8Type, &theAnswer); } return theAnswer; } SInt32 GetSInt32() const { SInt32 theAnswer = 0; if(mCFNumber != NULL) { CFNumberGetValue(mCFNumber, kCFNumberSInt32Type, &theAnswer); } return theAnswer; } UInt32 GetUInt32() const { UInt32 theAnswer = 0; if(mCFNumber != NULL) { CFNumberGetValue(mCFNumber, kCFNumberSInt32Type, &theAnswer); } return theAnswer; } Float32 GetFloat32() const { Float32 theAnswer = 0.0f; if(mCFNumber != NULL) { CFNumberGetValue(mCFNumber, kCFNumberFloat32Type, &theAnswer); } return theAnswer; } Float32 GetFixed32() const; Float64 GetFixed64() const; SInt64 GetSInt64() const { SInt64 theAnswer = 0; if(mCFNumber != NULL) { CFNumberGetValue(mCFNumber, kCFNumberSInt64Type, &theAnswer); } return theAnswer; } CACFNumber(const void*); // prevent accidental instantiation with a pointer via bool constructor }; #endif ================================================ FILE: BGMApp/PublicUtility/CACFString.cpp ================================================ /* File: CACFString.cpp Abstract: CACFString.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= #include "CACFString.h" //============================================================================= // CACFString //============================================================================= UInt32 CACFString::GetStringByteLength(CFStringRef inCFString, CFStringEncoding inEncoding) { CFIndex theAnswer = 0; if(inCFString != NULL) { CFRange theRange = { 0, CFStringGetLength(inCFString) }; CFStringGetBytes(inCFString, theRange, inEncoding, 0, false, NULL, 0x7FFFFFFF, &theAnswer); } return UInt32(theAnswer); } void CACFString::GetCString(CFStringRef inCFString, char* outString, UInt32& ioStringSize, CFStringEncoding inEncoding) { if(ioStringSize > 0) { if(inCFString != NULL) { CFIndex theLength = 0; CFRange theRange = { 0, CFStringGetLength(inCFString) }; CFStringGetBytes(inCFString, theRange, inEncoding, 0, false, (UInt8*)outString, static_cast(ioStringSize - 1), &theLength); outString[theLength] = 0; ioStringSize = ToUInt32(theLength) + 1; } else { outString[0] = 0; ioStringSize = 1; } } } void CACFString::GetUnicodeString(CFStringRef inCFString, UInt16* outString, UInt32& ioStringSize) { if(ioStringSize > 0) { if(inCFString != NULL) { CFRange theStringRange = { 0, CFStringGetLength(inCFString) }; if(static_cast(theStringRange.length) > ioStringSize) { theStringRange.length = static_cast(ioStringSize); } CFStringGetCharacters(inCFString, theStringRange, outString); ioStringSize = ToUInt32(theStringRange.length); } else { outString[0] = 0; ioStringSize = 0; } } } ================================================ FILE: BGMApp/PublicUtility/CACFString.h ================================================ /* File: CACFString.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CACFString_h__) #define __CACFString_h__ //============================================================================= // Includes //============================================================================= #include "CADebugMacros.h" #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #include #else #include #include #endif //============================================================================= // CACFString // // Notes // - Using the AssignWithoutRetain() method will fool the static analyzer into thinking that the // CFString being assigned will be leaked. This is because the static analyzer is not smart // enough to understand that the destructor will release the object. //============================================================================= class CACFString { // Construction/Destruction public: CACFString() : mCFString(NULL), mWillRelease(true) {} explicit CACFString(CFStringRef inCFString) : mCFString(inCFString), mWillRelease(true) {} CACFString(const char* inCString) : mCFString(CFStringCreateWithCString(NULL, inCString, kCFStringEncodingASCII)), mWillRelease(true) {} CACFString(CFStringRef inCFString, bool inWillRelease) : mCFString(inCFString), mWillRelease(inWillRelease) {} CACFString(const char* inCString, bool inWillRelease) : mCFString(CFStringCreateWithCString(NULL, inCString, kCFStringEncodingASCII)), mWillRelease(inWillRelease) {} CACFString(const char* inCString, CFStringEncoding inCStringEncoding, bool inWillRelease = true) : mCFString(CFStringCreateWithCString(NULL, inCString, inCStringEncoding)), mWillRelease(inWillRelease) {} ~CACFString() { Release(); } CACFString(const CACFString& inString) : mCFString(inString.mCFString), mWillRelease(inString.mWillRelease) { Retain(); } CACFString& operator=(const CACFString& inString) { if (inString.mCFString != mCFString) { Release(); mCFString = inString.mCFString; mWillRelease = inString.mWillRelease; Retain(); } return *this; } CACFString& operator=(CFStringRef inCFString) { if (inCFString != mCFString) { Release(); mCFString = inCFString; } mWillRelease = true; Retain(); return *this; } void AssignWithoutRetain(CFStringRef inCFString) { if (inCFString != mCFString) { Release(); mCFString = inCFString; } mWillRelease = true; } private: void Retain() { if(mWillRelease && (mCFString != NULL)) { CFRetain(mCFString); } } void Release() { if(mWillRelease && (mCFString != NULL)) { CFRelease(mCFString); } } CFStringRef mCFString; bool mWillRelease; // Operations public: void AllowRelease() { mWillRelease = true; } void DontAllowRelease() { mWillRelease = false; } bool IsValid() const { return mCFString != NULL; } bool IsEqualTo(CFStringRef inString) const { bool theAnswer = false; if(mCFString != NULL) { theAnswer = CFStringCompare(inString, mCFString, 0) == kCFCompareEqualTo; } return theAnswer; } bool StartsWith(CFStringRef inString) const { bool theAnswer = false; if(mCFString != NULL) { theAnswer = CFStringHasPrefix(mCFString, inString); } return theAnswer; } bool EndsWith(CFStringRef inString) const { bool theAnswer = false; if(mCFString != NULL) { theAnswer = CFStringHasSuffix(mCFString, inString); } return theAnswer; } // Value Access public: CFStringRef GetCFString() const { return mCFString; } CFStringRef CopyCFString() const { if(mCFString != NULL) { CFRetain(mCFString); } return mCFString; } const CFStringRef* GetPointerToStorage() const { return &mCFString; } CFStringRef& GetStorage() { Release(); mWillRelease = true; return mCFString; } UInt32 GetLength() const { UInt32 theAnswer = 0; if(mCFString != NULL) { theAnswer = ToUInt32(CFStringGetLength(mCFString)); } return theAnswer; } UInt32 GetByteLength(CFStringEncoding inEncoding = kCFStringEncodingUTF8) const { UInt32 theAnswer = 0; if(mCFString != NULL) { theAnswer = GetStringByteLength(mCFString, inEncoding); } return theAnswer; } void GetCString(char* outString, UInt32& ioStringSize, CFStringEncoding inEncoding = kCFStringEncodingUTF8) const { GetCString(mCFString, outString, ioStringSize, inEncoding); } void GetUnicodeString(UInt16* outString, UInt32& ioStringSize) const { GetUnicodeString(mCFString, outString, ioStringSize); } SInt32 GetAsInteger() { return GetAsInteger(mCFString); } Float64 GetAsFloat64() { return GetAsFloat64(mCFString); } static UInt32 GetStringLength(CFStringRef inCFString) { UInt32 theAnswer = 0; if(inCFString != NULL) { theAnswer = ToUInt32(CFStringGetLength(inCFString)); } return theAnswer; } static UInt32 GetStringByteLength(CFStringRef inCFString, CFStringEncoding inEncoding = kCFStringEncodingUTF8); static void GetCString(CFStringRef inCFString, char* outString, UInt32& ioStringSize, CFStringEncoding inEncoding = kCFStringEncodingUTF8); static void GetUnicodeString(CFStringRef inCFString, UInt16* outString, UInt32& ioStringSize); static SInt32 GetAsInteger(CFStringRef inCFString) { SInt32 theAnswer = 0; if(inCFString != NULL){ theAnswer = CFStringGetIntValue(inCFString); } return theAnswer; } static Float64 GetAsFloat64(CFStringRef inCFString) { Float64 theAnswer = 0; if(inCFString != NULL){ theAnswer = CFStringGetDoubleValue(inCFString); } return theAnswer; } }; inline bool operator<(const CACFString& x, const CACFString& y) { return CFStringCompare(x.GetCFString(), y.GetCFString(), 0) == kCFCompareLessThan; } inline bool operator==(const CACFString& x, const CACFString& y) { return CFStringCompare(x.GetCFString(), y.GetCFString(), 0) == kCFCompareEqualTo; } inline bool operator!=(const CACFString& x, const CACFString& y) { return !(x == y); } inline bool operator<=(const CACFString& x, const CACFString& y) { return (x < y) || (x == y); } inline bool operator>=(const CACFString& x, const CACFString& y) { return !(x < y); } inline bool operator>(const CACFString& x, const CACFString& y) { return !((x < y) || (x == y)); } inline bool operator<(const CACFString& x, CFStringRef y) { return CFStringCompare(x.GetCFString(), y, 0) == kCFCompareLessThan; } inline bool operator==(const CACFString& x, CFStringRef y) { return CFStringCompare(x.GetCFString(), y, 0) == kCFCompareEqualTo; } inline bool operator!=(const CACFString& x, CFStringRef y) { return !(x == y); } inline bool operator<=(const CACFString& x, CFStringRef y) { return (x < y) || (x == y); } inline bool operator>=(const CACFString& x, CFStringRef y) { return !(x < y); } inline bool operator>(const CACFString& x, CFStringRef y) { return !((x < y) || (x == y)); } inline bool operator<(CFStringRef x, const CACFString& y) { return CFStringCompare(x, y.GetCFString(), 0) == kCFCompareLessThan; } inline bool operator==(CFStringRef x, const CACFString& y) { return CFStringCompare(x, y.GetCFString(), 0) == kCFCompareEqualTo; } inline bool operator!=(CFStringRef x, const CACFString& y) { return !(x == y); } inline bool operator<=(CFStringRef x, const CACFString& y) { return (x < y) || (x == y); } inline bool operator>=(CFStringRef x, const CACFString& y) { return !(x < y); } inline bool operator>(CFStringRef x, const CACFString& y) { return !((x < y) || (x == y)); } //============================================================================= // CACFMutableString //============================================================================= class CACFMutableString { // Construction/Destruction public: CACFMutableString() : mCFMutableString(NULL), mWillRelease(true) {} CACFMutableString(CFMutableStringRef inCFMutableString, bool inWillRelease = true) : mCFMutableString(inCFMutableString), mWillRelease(inWillRelease) {} CACFMutableString(CFStringRef inStringToCopy, bool /*inMakeCopy*/, bool inWillRelease = true) : mCFMutableString(CFStringCreateMutableCopy(NULL, 0, inStringToCopy)), mWillRelease(inWillRelease) {} CACFMutableString(const char* inCString, bool inWillRelease = true) : mCFMutableString(NULL), mWillRelease(inWillRelease) { CACFString theString(inCString); mCFMutableString = CFStringCreateMutableCopy(NULL, 0, theString.GetCFString()); } CACFMutableString(const char* inCString, CFStringEncoding inCStringEncoding, bool inWillRelease = true) : mCFMutableString(NULL), mWillRelease(inWillRelease) { CACFString theString(inCString, inCStringEncoding); mCFMutableString = CFStringCreateMutableCopy(NULL, 0, theString.GetCFString()); } ~CACFMutableString() { Release(); } CACFMutableString(const CACFMutableString& inString) : mCFMutableString(inString.mCFMutableString), mWillRelease(inString.mWillRelease) { Retain(); } CACFMutableString& operator=(const CACFMutableString& inString) { Release(); mCFMutableString = inString.mCFMutableString; mWillRelease = inString.mWillRelease; Retain(); return *this; } CACFMutableString& operator=(CFMutableStringRef inCFMutableString) { Release(); mCFMutableString = inCFMutableString; mWillRelease = true; return *this; } private: void Retain() { if(mWillRelease && (mCFMutableString != NULL)) { CFRetain(mCFMutableString); } } void Release() { if(mWillRelease && (mCFMutableString != NULL)) { CFRelease(mCFMutableString); } } CFMutableStringRef mCFMutableString; bool mWillRelease; // Operations public: void AllowRelease() { mWillRelease = true; } void DontAllowRelease() { mWillRelease = false; } bool IsValid() { return mCFMutableString != NULL; } bool IsEqualTo(CFStringRef inString) const { bool theAnswer = false; if(mCFMutableString != NULL) { theAnswer = CFStringCompare(inString, mCFMutableString, 0) == kCFCompareEqualTo; } return theAnswer; } bool StartsWith(CFStringRef inString) const { bool theAnswer = false; if(mCFMutableString != NULL) { theAnswer = CFStringHasPrefix(mCFMutableString, inString); } return theAnswer; } bool EndsWith(CFStringRef inString) const { bool theAnswer = false; if(mCFMutableString != NULL) { theAnswer = CFStringHasSuffix(mCFMutableString, inString); } return theAnswer; } void Append(CFStringRef inString) { if(mCFMutableString != NULL) { CFStringAppend(mCFMutableString, inString); } } // Value Access public: CFMutableStringRef GetCFMutableString() const { return mCFMutableString; } CFMutableStringRef CopyCFMutableString() const { if(mCFMutableString != NULL) { CFRetain(mCFMutableString); } return mCFMutableString; } UInt32 GetLength() const { UInt32 theAnswer = 0; if(mCFMutableString != NULL) { theAnswer = ToUInt32(CFStringGetLength(mCFMutableString)); } return theAnswer; } UInt32 GetByteLength(CFStringEncoding inEncoding = kCFStringEncodingUTF8) const { UInt32 theAnswer = 0; if(mCFMutableString != NULL) { theAnswer = CACFString::GetStringByteLength(mCFMutableString, inEncoding); } return theAnswer; } void GetCString(char* outString, UInt32& ioStringSize, CFStringEncoding inEncoding = kCFStringEncodingUTF8) const { CACFString::GetCString(mCFMutableString, outString, ioStringSize, inEncoding); } void GetUnicodeString(UInt16* outString, UInt32& ioStringSize) const { CACFString::GetUnicodeString(mCFMutableString, outString, ioStringSize); } SInt32 GetAsInteger() { return CACFString::GetAsInteger(mCFMutableString); } Float64 GetAsFloat64() { return CACFString::GetAsFloat64(mCFMutableString); } }; #endif ================================================ FILE: BGMApp/PublicUtility/CADebugMacros.cpp ================================================ // 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 . // // CADebugMacros.cpp // PublicUtility // // Copyright (C) 2014 Apple Inc. All Rights Reserved. // Copyright © 2016, 2017, 2020 Kyle Neideck // // Original license header follows. // /* File: CADebugMacros.cpp Abstract: CADebugMacros.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #include "CADebugMacros.h" #include #if TARGET_API_MAC_OSX #include #endif #if DEBUG #include void DebugPrint(const char *fmt, ...) { va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); } #endif // DEBUG void LogError(const char *fmt, ...) { va_list args; va_start(args, fmt); vLogError(fmt, args); va_end(args); } void vLogError(const char *fmt, va_list args) { #if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog printf("[ERROR] "); vprintf(fmt, args); printf("\n"); #else vsyslog(LOG_ERR, fmt, args); #endif #if DEBUG CADebuggerStop(); #endif } void LogWarning(const char *fmt, ...) { va_list args; va_start(args, fmt); vLogWarning(fmt, args); va_end(args); } void vLogWarning(const char *fmt, va_list args) { #if (DEBUG || !TARGET_API_MAC_OSX) && !CoreAudio_UseSysLog printf("[WARNING] "); vprintf(fmt, args); printf("\n"); #else 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. #endif } ================================================ FILE: BGMApp/PublicUtility/CADebugMacros.h ================================================ // 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 . // // CADebugMacros.h // PublicUtility // // Copyright (C) 2014 Apple Inc. All Rights Reserved. // Copyright © 2016, 2020 Kyle Neideck // // Original license header follows. // /* File: CADebugMacros.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CADebugMacros_h__) #define __CADebugMacros_h__ //============================================================================= // Includes //============================================================================= #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include "CoreAudioTypes.h" #endif #include "CADebugPrintf.h" #include //============================================================================= // CADebugMacros //============================================================================= //#define CoreAudio_StopOnFailure 1 //#define CoreAudio_TimeStampMessages 1 //#define CoreAudio_ThreadStampMessages 1 //#define CoreAudio_FlushDebugMessages 1 #if TARGET_RT_BIG_ENDIAN #define CA4CCToCString(the4CC) { ((char*)&the4CC)[0], ((char*)&the4CC)[1], ((char*)&the4CC)[2], ((char*)&the4CC)[3], 0 } #define CACopy4CCToCString(theCString, the4CC) { theCString[0] = ((char*)&the4CC)[0]; theCString[1] = ((char*)&the4CC)[1]; theCString[2] = ((char*)&the4CC)[2]; theCString[3] = ((char*)&the4CC)[3]; theCString[4] = 0; } #else #define CA4CCToCString(the4CC) { ((char*)&the4CC)[3], ((char*)&the4CC)[2], ((char*)&the4CC)[1], ((char*)&the4CC)[0], 0 } #define CACopy4CCToCString(theCString, the4CC) { theCString[0] = ((char*)&the4CC)[3]; theCString[1] = ((char*)&the4CC)[2]; theCString[2] = ((char*)&the4CC)[1]; theCString[3] = ((char*)&the4CC)[0]; theCString[4] = 0; } #endif // This is a macro that does a sizeof and casts the result to a UInt32. This is useful for all the // places where -wshorten64-32 catches assigning a sizeof expression to a UInt32. // For want of a better place to park this, we'll park it here. #define SizeOf32(X) ((UInt32)sizeof(X)) // This is a macro that does a offsetof and casts the result to a UInt32. This is useful for all the // places where -wshorten64-32 catches assigning an offsetof expression to a UInt32. // For want of a better place to park this, we'll park it here. #define OffsetOf32(X, Y) ((UInt32)offsetof(X, Y)) // This macro casts the expression to a UInt32. It is called out specially to allow us to track casts // that have been added purely to avert -wshorten64-32 warnings on 64 bit platforms. // For want of a better place to park this, we'll park it here. #define ToUInt32(X) ((UInt32)(X)) #define ToSInt32(X) ((SInt32)(X)) #pragma mark Basic Definitions // basic debugging print routines #if TARGET_OS_MAC && !TARGET_API_MAC_CARBON extern void DebugStr(const unsigned char* debuggerMsg); #define DebugMessage(msg) DebugStr("\p"msg) #define DebugMessageN1(msg, N1) #define DebugMessageN2(msg, N1, N2) #define DebugMessageN3(msg, N1, N2, N3) #else #if (CoreAudio_FlushDebugMessages && !CoreAudio_UseSysLog) || defined(CoreAudio_UseSideFile) #define FlushRtn ,fflush(DebugPrintfFile) #else #define FlushRtn #endif #if CoreAudio_ThreadStampMessages #include #include "CAHostTimeBase.h" #if TARGET_RT_64_BIT #define DebugPrintfThreadIDFormat "%16p" #else #define DebugPrintfThreadIDFormat "%8p" #endif #define DebugMsg(inFormat, ...) DebugPrintf("%17qd: " DebugPrintfThreadIDFormat " " inFormat, CAHostTimeBase::GetCurrentTimeInNanos(), pthread_self(), ## __VA_ARGS__) FlushRtn #elif CoreAudio_TimeStampMessages #include "CAHostTimeBase.h" #define DebugMsg(inFormat, ...) DebugPrintf("%17qd: " inFormat, CAHostTimeBase::GetCurrentTimeInNanos(), ## __VA_ARGS__) FlushRtn #else #define DebugMsg(inFormat, ...) DebugPrintf(inFormat, ## __VA_ARGS__) FlushRtn #endif #endif #if DEBUG || CoreAudio_Debug // can be used to break into debugger immediately, also see CADebugger #define BusError() { long* p=NULL; *p=0; } void DebugPrint(const char *fmt, ...); // can be used like printf #ifndef DEBUGPRINT #define DEBUGPRINT(msg) DebugPrint msg // have to double-parenthesize arglist (see Debugging.h) #endif #if VERBOSE #define vprint(msg) DEBUGPRINT(msg) #else #define vprint(msg) #endif // Original macro keeps its function of turning on and off use of CADebuggerStop() for both asserts and throws. // For backwards compat, it overrides any setting of the two sub-macros. #if CoreAudio_StopOnFailure #include "CADebugger.h" #undef CoreAudio_StopOnAssert #define CoreAudio_StopOnAssert 1 #undef CoreAudio_StopOnThrow #define CoreAudio_StopOnThrow 1 #define STOP CADebuggerStop() #else #define STOP #endif #if CoreAudio_StopOnAssert #if !CoreAudio_StopOnFailure #include "CADebugger.h" #define STOP #endif #define __ASSERT_STOP CADebuggerStop() #else #define __ASSERT_STOP #endif #if CoreAudio_StopOnThrow #if !CoreAudio_StopOnFailure #include "CADebugger.h" #define STOP #endif #define __THROW_STOP CADebuggerStop() #else #define __THROW_STOP #endif #else #ifndef DEBUGPRINT #define DEBUGPRINT(msg) #endif #define vprint(msg) #define STOP #define __ASSERT_STOP #define __THROW_STOP #endif // Old-style numbered DebugMessage calls are implemented in terms of DebugMsg() now #define DebugMessage(msg) DebugMsg(msg) #define DebugMessageN1(msg, N1) DebugMsg(msg, N1) #define DebugMessageN2(msg, N1, N2) DebugMsg(msg, N1, N2) #define DebugMessageN3(msg, N1, N2, N3) DebugMsg(msg, N1, N2, N3) #define DebugMessageN4(msg, N1, N2, N3, N4) DebugMsg(msg, N1, N2, N3, N4) #define DebugMessageN5(msg, N1, N2, N3, N4, N5) DebugMsg(msg, N1, N2, N3, N4, N5) #define DebugMessageN6(msg, N1, N2, N3, N4, N5, N6) DebugMsg(msg, N1, N2, N3, N4, N5, N6) #define DebugMessageN7(msg, N1, N2, N3, N4, N5, N6, N7) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7) #define DebugMessageN8(msg, N1, N2, N3, N4, N5, N6, N7, N8) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8) #define DebugMessageN9(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9) DebugMsg(msg, N1, N2, N3, N4, N5, N6, N7, N8, N9) // BGM edit: Added __printflike and va_list versions. void LogError(const char *fmt, ...) __printflike(1, 2); // writes to syslog (and stderr if debugging) void vLogError(const char *fmt, va_list args); void LogWarning(const char *fmt, ...) __printflike(1, 2); // writes to syslog (and stderr if debugging) void vLogWarning(const char *fmt, va_list args); #define NO_ACTION (void)0 #if DEBUG || CoreAudio_Debug #pragma mark Debug Macros #define Assert(inCondition, inMessage) \ if(!(inCondition)) \ { \ DebugMessage(inMessage); \ __ASSERT_STOP; \ } #define AssertFileLine(inCondition, inMessage) \ if(!(inCondition)) \ { \ DebugMessageN3("%s, line %d: %s", __FILE__, __LINE__, inMessage); \ __ASSERT_STOP; \ } #define AssertNoError(inError, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ char __4CC[5] = CA4CCToCString(__Err); \ DebugMessageN2(inMessage ", Error: %d (%s)", (int)__Err, __4CC); \ __ASSERT_STOP; \ } \ } #define AssertNoKernelError(inError, inMessage) \ { \ unsigned int __Err = (unsigned int)(inError); \ if(__Err != 0) \ { \ DebugMessageN1(inMessage ", Error: 0x%X", __Err); \ __ASSERT_STOP; \ } \ } #define AssertNotNULL(inPtr, inMessage) \ { \ if((inPtr) == NULL) \ { \ DebugMessage(inMessage); \ __ASSERT_STOP; \ } \ } #define FailIf(inCondition, inHandler, inMessage) \ if(inCondition) \ { \ DebugMessage(inMessage); \ STOP; \ goto inHandler; \ } #define FailWithAction(inCondition, inAction, inHandler, inMessage) \ if(inCondition) \ { \ DebugMessage(inMessage); \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfNULL(inPointer, inAction, inHandler, inMessage) \ if((inPointer) == NULL) \ { \ DebugMessage(inMessage); \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfKernelError(inKernelError, inAction, inHandler, inMessage) \ { \ unsigned int __Err = (inKernelError); \ if(__Err != 0) \ { \ DebugMessageN1(inMessage ", Error: 0x%X", __Err); \ STOP; \ { inAction; } \ goto inHandler; \ } \ } #define FailIfError(inError, inAction, inHandler, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ char __4CC[5] = CA4CCToCString(__Err); \ DebugMessageN2(inMessage ", Error: %ld (%s)", (long int)__Err, __4CC); \ STOP; \ { inAction; } \ goto inHandler; \ } \ } #define FailIfNoMessage(inCondition, inHandler, inMessage) \ if(inCondition) \ { \ STOP; \ goto inHandler; \ } #define FailWithActionNoMessage(inCondition, inAction, inHandler, inMessage) \ if(inCondition) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfNULLNoMessage(inPointer, inAction, inHandler, inMessage) \ if((inPointer) == NULL) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfKernelErrorNoMessage(inKernelError, inAction, inHandler, inMessage) \ { \ unsigned int __Err = (inKernelError); \ if(__Err != 0) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } \ } #define FailIfErrorNoMessage(inError, inAction, inHandler, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } \ } #if defined(__cplusplus) #define Throw(inException) __THROW_STOP; throw (inException) #define ThrowIf(inCondition, inException, inMessage) \ if(inCondition) \ { \ DebugMessage(inMessage); \ Throw(inException); \ } #define ThrowIfNULL(inPointer, inException, inMessage) \ if((inPointer) == NULL) \ { \ DebugMessage(inMessage); \ Throw(inException); \ } #define ThrowIfKernelError(inKernelError, inException, inMessage) \ { \ int __Err = (inKernelError); \ if(__Err != 0) \ { \ DebugMessageN1(inMessage ", Error: 0x%X", __Err); \ Throw(inException); \ } \ } #define ThrowIfError(inError, inException, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ char __4CC[5] = CA4CCToCString(__Err); \ DebugMessageN2(inMessage ", Error: %d (%s)", (int)__Err, __4CC); \ Throw(inException); \ } \ } #if TARGET_OS_WIN32 #define ThrowIfWinError(inError, inException, inMessage) \ { \ HRESULT __Err = (inError); \ if(FAILED(__Err)) \ { \ DebugMessageN2(inMessage ", Code: %d, Facility: 0x%X", HRESULT_CODE(__Err), HRESULT_FACILITY(__Err)); \ Throw(inException); \ } \ } #endif #define SubclassResponsibility(inMethodName, inException) \ { \ DebugMessage(inMethodName": Subclasses must implement this method"); \ Throw(inException); \ } #endif // defined(__cplusplus) #else #pragma mark Release Macros #define Assert(inCondition, inMessage) \ if(!(inCondition)) \ { \ __ASSERT_STOP; \ } #define AssertFileLine(inCondition, inMessage) Assert(inCondition, inMessage) #define AssertNoError(inError, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ __ASSERT_STOP; \ } \ } #define AssertNoKernelError(inError, inMessage) \ { \ unsigned int __Err = (unsigned int)(inError); \ if(__Err != 0) \ { \ __ASSERT_STOP; \ } \ } #define AssertNotNULL(inPtr, inMessage) \ { \ if((inPtr) == NULL) \ { \ __ASSERT_STOP; \ } \ } #define FailIf(inCondition, inHandler, inMessage) \ if(inCondition) \ { \ STOP; \ goto inHandler; \ } #define FailWithAction(inCondition, inAction, inHandler, inMessage) \ if(inCondition) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfNULL(inPointer, inAction, inHandler, inMessage) \ if((inPointer) == NULL) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfKernelError(inKernelError, inAction, inHandler, inMessage) \ if((inKernelError) != 0) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfError(inError, inAction, inHandler, inMessage) \ if((inError) != 0) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfNoMessage(inCondition, inHandler, inMessage) \ if(inCondition) \ { \ STOP; \ goto inHandler; \ } #define FailWithActionNoMessage(inCondition, inAction, inHandler, inMessage) \ if(inCondition) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfNULLNoMessage(inPointer, inAction, inHandler, inMessage) \ if((inPointer) == NULL) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } #define FailIfKernelErrorNoMessage(inKernelError, inAction, inHandler, inMessage) \ { \ unsigned int __Err = (inKernelError); \ if(__Err != 0) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } \ } #define FailIfErrorNoMessage(inError, inAction, inHandler, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ STOP; \ { inAction; } \ goto inHandler; \ } \ } #if defined(__cplusplus) #define Throw(inException) __THROW_STOP; throw (inException) #define ThrowIf(inCondition, inException, inMessage) \ if(inCondition) \ { \ Throw(inException); \ } #define ThrowIfNULL(inPointer, inException, inMessage) \ if((inPointer) == NULL) \ { \ Throw(inException); \ } #define ThrowIfKernelError(inKernelError, inException, inMessage) \ { \ int __Err = (inKernelError); \ if(__Err != 0) \ { \ Throw(inException); \ } \ } #define ThrowIfError(inError, inException, inMessage) \ { \ SInt32 __Err = (inError); \ if(__Err != 0) \ { \ Throw(inException); \ } \ } #if TARGET_OS_WIN32 #define ThrowIfWinError(inError, inException, inMessage) \ { \ HRESULT __Err = (inError); \ if(FAILED(__Err)) \ { \ Throw(inException); \ } \ } #endif #define SubclassResponsibility(inMethodName, inException) \ { \ Throw(inException); \ } #endif // defined(__cplusplus) #endif // DEBUG || CoreAudio_Debug #endif ================================================ FILE: BGMApp/PublicUtility/CADebugPrintf.cpp ================================================ // 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 . // // CADebugPrintf.cpp // PublicUtility // // Copyright (C) 2014 Apple Inc. All Rights Reserved. // Copyright © 2020 Kyle Neideck // // Original license header follows. // /* File: CADebugPrintf.cpp Abstract: CADebugPrintf.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //================================================================================================== // Includes //================================================================================================== // Self Include #include "CADebugPrintf.h" #if TARGET_OS_WIN32 #include #include #include extern "C" int CAWin32DebugPrintf(char* inFormat, ...) { if (BGMDebugLoggingIsEnabled()) { char theMessage[1024]; va_list theArguments; va_start(theArguments, inFormat); _vsnprintf(theMessage, 1024, inFormat, theArguments); va_end(theArguments); OutputDebugString(theMessage); } return 0; } #endif #if defined(CoreAudio_UseSideFile) #include FILE* sDebugPrintfSideFile = NULL; extern "C" void OpenDebugPrintfSideFile() { if(sDebugPrintfSideFile == NULL) { char theFileName[1024]; snprintf(theFileName, sizeof(theFileName), CoreAudio_UseSideFile, getpid()); sDebugPrintfSideFile = fopen(theFileName, "a+"); DebugPrintfRtn(DebugPrintfFileComma "\n------------------------------\n"); } } #endif ================================================ FILE: BGMApp/PublicUtility/CADebugPrintf.h ================================================ /* File: CADebugPrintf.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CADebugPrintf_h__) #define __CADebugPrintf_h__ //============================================================================= // Includes //============================================================================= #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include "CoreAudioTypes.h" #endif #include "BGMDebugLogging.h" //============================================================================= // Macros to redirect debugging output to various logging services //============================================================================= //#define CoreAudio_UseSysLog 1 //#define CoreAudio_UseSideFile "/CoreAudio-%d.txt" #if TARGET_OS_WIN32 #if defined(__cplusplus) extern "C" #endif extern int CAWin32DebugPrintf(char* inFormat, ...); #define DebugPrintfRtn CAWin32DebugPrintf #define DebugPrintfFile #define DebugPrintfLineEnding "\n" #define DebugPrintfFileComma #else #if CoreAudio_UseSysLog #include #define DebugPrintfRtn syslog #define DebugPrintfFile LOG_NOTICE #define DebugPrintfLineEnding "" #define DebugPrintfFileComma DebugPrintfFile, #elif defined(CoreAudio_UseSideFile) #include #if defined(__cplusplus) extern "C" #endif void OpenDebugPrintfSideFile(); extern FILE* sDebugPrintfSideFile; #define DebugPrintfRtn fprintf #define DebugPrintfFile ((sDebugPrintfSideFile != NULL) ? sDebugPrintfSideFile : stderr) #define DebugPrintfLineEnding "\n" #define DebugPrintfFileComma DebugPrintfFile, #else #include #define DebugPrintfRtn fprintf #define DebugPrintfFile stderr #define DebugPrintfLineEnding "\n" #define DebugPrintfFileComma DebugPrintfFile, #endif #endif #define DebugPrintf(inFormat, ...) \ do { \ if (BGMDebugLoggingIsEnabled()) { \ DebugPrintfRtn(DebugPrintfFileComma inFormat DebugPrintfLineEnding, ## __VA_ARGS__); \ } \ } while (0) #endif ================================================ FILE: BGMApp/PublicUtility/CADebugger.cpp ================================================ /* File: CADebugger.cpp Abstract: CADebugger.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= #include "CADebugger.h" //============================================================================= // CADebugger //============================================================================= #if TARGET_API_MAC_OSX #include #include #include bool CAIsDebuggerAttached(void) { int mib[4]; struct kinfo_proc info; size_t size; mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; mib[3] = getpid(); size = sizeof(info); info.kp_proc.p_flag = 0; sysctl(mib, 4, &info, &size, NULL, 0); return (info.kp_proc.p_flag & P_TRACED) == P_TRACED; } #endif void CADebuggerStop(void) { #if CoreAudio_Debug #if TARGET_API_MAC_OSX if(CAIsDebuggerAttached()) { #if defined(__i386__) || defined(__x86_64__) asm("int3"); #else __builtin_trap(); #endif } else { abort(); } #else __debugbreak(); #endif #endif } ================================================ FILE: BGMApp/PublicUtility/CADebugger.h ================================================ /* File: CADebugger.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CADebugger_h__) #define __CADebugger_h__ //============================================================================= // Includes //============================================================================= #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include #endif //============================================================================= // 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); #ifdef __cplusplus } #endif #endif ================================================ FILE: BGMApp/PublicUtility/CAException.h ================================================ /* File: CAException.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAException_h__) #define __CAException_h__ //============================================================================= // Includes //============================================================================= #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include "CoreAudioTypes.h" #endif //============================================================================= // CAException //============================================================================= class CAException { public: CAException(OSStatus inError) : mError(inError) {} CAException(const CAException& inException) : mError(inException.mError) {} CAException& operator=(const CAException& inException) { mError = inException.mError; return *this; } ~CAException() {} OSStatus GetError() const { return mError; } protected: OSStatus mError; }; #define CATry try{ #define CACatch } catch(...) {} #define CASwallowException(inExpression) try { inExpression; } catch(...) {} #endif ================================================ FILE: BGMApp/PublicUtility/CAHALAudioDevice.cpp ================================================ /* File: CAHALAudioDevice.cpp Abstract: CAHALAudioDevice.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //================================================================================================== // Includes //================================================================================================== // Self Include #include "CAHALAudioDevice.h" // PublicUtility Includes #include "CAAutoDisposer.h" #include "CAHALAudioStream.h" #include "CAHALAudioSystemObject.h" #include "CAPropertyAddress.h" //================================================================================================== // CAHALAudioDevice //================================================================================================== CAHALAudioDevice::CAHALAudioDevice(AudioObjectID inAudioDevice) : CAHALAudioObject(inAudioDevice) { } CAHALAudioDevice::CAHALAudioDevice(CFStringRef inUID) : CAHALAudioObject(CAHALAudioSystemObject().GetAudioDeviceForUID(inUID)) { } CAHALAudioDevice::~CAHALAudioDevice() { } CFStringRef CAHALAudioDevice::CopyDeviceUID() const { CAPropertyAddress theAddress(kAudioDevicePropertyDeviceUID); return GetPropertyData_CFString(theAddress, 0, NULL); } bool CAHALAudioDevice::HasModelUID() const { CAPropertyAddress theAddress(kAudioDevicePropertyModelUID); return HasProperty(theAddress); } CFStringRef CAHALAudioDevice::CopyModelUID() const { CAPropertyAddress theAddress(kAudioDevicePropertyModelUID); return GetPropertyData_CFString(theAddress, 0, NULL); } CFStringRef CAHALAudioDevice::CopyConfigurationApplicationBundleID() const { CAPropertyAddress theAddress(kAudioDevicePropertyConfigurationApplication); return GetPropertyData_CFString(theAddress, 0, NULL); } CFURLRef CAHALAudioDevice::CopyIconLocation() const { CFURLRef theAnswer = NULL; CAPropertyAddress theAddress(kAudioDevicePropertyIcon); UInt32 theSize = sizeof(CFURLRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } UInt32 CAHALAudioDevice::GetTransportType() const { CAPropertyAddress theAddress(kAudioDevicePropertyTransportType); return GetPropertyData_UInt32(theAddress, 0, NULL); } bool CAHALAudioDevice::CanBeDefaultDevice(bool inIsInput, bool inIsSystem) const { CAPropertyAddress theAddress(inIsSystem ? kAudioDevicePropertyDeviceCanBeDefaultSystemDevice : kAudioDevicePropertyDeviceCanBeDefaultDevice, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); return GetPropertyData_UInt32(theAddress, 0, NULL) != 0; } bool CAHALAudioDevice::HasDevicePlugInStatus() const { CAPropertyAddress theAddress(kAudioDevicePropertyPlugIn); return HasProperty(theAddress); } OSStatus CAHALAudioDevice::GetDevicePlugInStatus() const { CAPropertyAddress theAddress(kAudioDevicePropertyPlugIn); return GetPropertyData_UInt32(theAddress, 0, NULL); } bool CAHALAudioDevice::IsAlive() const { CAPropertyAddress theAddress(kAudioDevicePropertyDeviceIsAlive); return GetPropertyData_UInt32(theAddress, 0, NULL) != 0; } bool CAHALAudioDevice::IsHidden() const { CAPropertyAddress theAddress(kAudioDevicePropertyIsHidden); return GetPropertyData_UInt32(theAddress, 0, NULL) != 0; } pid_t CAHALAudioDevice::GetHogModeOwner() const { pid_t theAnswer = -1; CAPropertyAddress theAddress(kAudioDevicePropertyHogMode); if(HasProperty(theAddress)) { UInt32 theSize = sizeof(pid_t); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } bool CAHALAudioDevice::IsHogModeSettable() const { bool theAnswer = false; CAPropertyAddress theAddress(kAudioDevicePropertyHogMode); if(HasProperty(theAddress)) { theAnswer = IsPropertySettable(theAddress); } return theAnswer; } bool CAHALAudioDevice::TakeHogMode() { CAPropertyAddress theAddress(kAudioDevicePropertyHogMode); pid_t thePID = getpid(); if(HasProperty(theAddress)) { SetPropertyData(theAddress, 0, NULL, sizeof(pid_t), &thePID); } return thePID == getpid(); } void CAHALAudioDevice::ReleaseHogMode() { CAPropertyAddress theAddress(kAudioDevicePropertyHogMode); if(HasProperty(theAddress)) { pid_t thePID = -1; SetPropertyData(theAddress, 0, NULL, sizeof(pid_t), &thePID); } } bool CAHALAudioDevice::HasPreferredStereoChannels(bool inIsInput) const { CAPropertyAddress theAddress(kAudioDevicePropertyPreferredChannelsForStereo, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); return HasProperty(theAddress); } void CAHALAudioDevice::GetPreferredStereoChannels(bool inIsInput, UInt32& outLeft, UInt32& outRight) const { CAPropertyAddress theAddress(kAudioDevicePropertyPreferredChannelsForStereo, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theStereoPair[2] = { 0, 0 }; UInt32 theSize = 2 * sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, theStereoPair); outLeft = theStereoPair[0]; outRight = theStereoPair[1]; } void CAHALAudioDevice::SetPreferredStereoChannels(bool inIsInput, UInt32 inLeft, UInt32 inRight) { CAPropertyAddress theAddress(kAudioDevicePropertyPreferredChannelsForStereo, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theStereoPair[2] = { inLeft, inRight }; SetPropertyData(theAddress, 0, NULL, 2 * sizeof(UInt32), theStereoPair); } bool CAHALAudioDevice::HasPreferredChannelLayout(bool inIsInput) const { CAPropertyAddress theAddress(kAudioDevicePropertyPreferredChannelLayout, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); return HasProperty(theAddress); } void CAHALAudioDevice::GetPreferredChannelLayout(bool inIsInput, AudioChannelLayout& outChannelLayout) const { CAPropertyAddress theAddress(kAudioDevicePropertyPreferredChannelLayout, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theSize = (SizeOf32(AudioChannelLayout) - SizeOf32(AudioChannelDescription)) + GetTotalNumberChannels(inIsInput) * SizeOf32(AudioChannelDescription); GetPropertyData(theAddress, 0, NULL, theSize, &outChannelLayout); } void CAHALAudioDevice::SetPreferredStereoChannels(bool inIsInput, AudioChannelLayout& inChannelLayout) { CAPropertyAddress theAddress(kAudioDevicePropertyPreferredChannelLayout, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theSize = (SizeOf32(AudioChannelLayout) - SizeOf32(AudioChannelDescription)) + GetTotalNumberChannels(inIsInput) * SizeOf32(AudioChannelDescription); SetPropertyData(theAddress, 0, NULL, theSize, &inChannelLayout); } UInt32 CAHALAudioDevice::GetNumberRelatedAudioDevices() const { CAPropertyAddress theAddress(kAudioDevicePropertyRelatedDevices); UInt32 theAnswer = 0; if(HasProperty(theAddress)) { theAnswer = GetPropertyDataSize(theAddress, 0, NULL); theAnswer /= SizeOf32(AudioObjectID); } return theAnswer; } void CAHALAudioDevice::GetRelatedAudioDevices(UInt32& ioNumberRelatedDevices, AudioObjectID* outRelatedDevices) const { CAPropertyAddress theAddress(kAudioDevicePropertyRelatedDevices); if(HasProperty(theAddress)) { UInt32 theSize = ioNumberRelatedDevices * SizeOf32(AudioObjectID); GetPropertyData(theAddress, 0, NULL, theSize, outRelatedDevices); ioNumberRelatedDevices = theSize / SizeOf32(AudioObjectID); } else { UInt32 theSize = ioNumberRelatedDevices * SizeOf32(AudioObjectID); memset(outRelatedDevices, 0, theSize); ioNumberRelatedDevices = 0; } } AudioObjectID CAHALAudioDevice::GetRelatedAudioDeviceByIndex(UInt32 inIndex) const { AudioObjectID theAnswer = kAudioObjectUnknown; UInt32 theNumberRelatedDevices = GetNumberRelatedAudioDevices(); if((theNumberRelatedDevices > 0) && (inIndex < theNumberRelatedDevices)) { CAAutoArrayDelete theRelatedDeviceList(theNumberRelatedDevices); GetRelatedAudioDevices(theNumberRelatedDevices, theRelatedDeviceList); if((theNumberRelatedDevices > 0) && (inIndex < theNumberRelatedDevices)) { theAnswer = theRelatedDeviceList[inIndex]; } } return theAnswer; } UInt32 CAHALAudioDevice::GetNumberStreams(bool inIsInput) const { CAPropertyAddress theAddress(kAudioDevicePropertyStreams, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theAnswer = GetPropertyDataSize(theAddress, 0, NULL); theAnswer /= SizeOf32(AudioObjectID); return theAnswer; } void CAHALAudioDevice::GetStreams(bool inIsInput, UInt32& ioNumberStreams, AudioObjectID* outStreamList) const { CAPropertyAddress theAddress(kAudioDevicePropertyStreams, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theSize = ioNumberStreams * SizeOf32(AudioObjectID); GetPropertyData(theAddress, 0, NULL, theSize, outStreamList); ioNumberStreams = theSize / SizeOf32(AudioObjectID); } AudioObjectID CAHALAudioDevice::GetStreamByIndex(bool inIsInput, UInt32 inIndex) const { AudioObjectID theAnswer = kAudioObjectUnknown; UInt32 theNumberStreams = GetNumberStreams(inIsInput); if((theNumberStreams > 0) && (inIndex < theNumberStreams)) { CAAutoArrayDelete theStreamList(theNumberStreams); GetStreams(inIsInput, theNumberStreams, theStreamList); if((theNumberStreams > 0) && (inIndex < theNumberStreams)) { theAnswer = theStreamList[inIndex]; } } return theAnswer; } UInt32 CAHALAudioDevice::GetTotalNumberChannels(bool inIsInput) const { UInt32 theAnswer = 0; CAPropertyAddress theAddress(kAudioDevicePropertyStreamConfiguration, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); UInt32 theSize = GetPropertyDataSize(theAddress, 0, NULL); CAAutoFree theBufferList(theSize); GetPropertyData(theAddress, 0, NULL, theSize, theBufferList); for(UInt32 theIndex = 0; theIndex < theBufferList->mNumberBuffers; ++theIndex) { theAnswer += theBufferList->mBuffers[theIndex].mNumberChannels; } return theAnswer; } void CAHALAudioDevice::GetCurrentVirtualFormats(bool inIsInput, UInt32& ioNumberStreams, AudioStreamBasicDescription* outFormats) const { ioNumberStreams = std::min(ioNumberStreams, GetNumberStreams(inIsInput)); for(UInt32 theIndex = 0; theIndex < ioNumberStreams; ++theIndex) { CAHALAudioStream theStream(GetStreamByIndex(inIsInput, theIndex)); theStream.GetCurrentVirtualFormat(outFormats[theIndex]); } } void CAHALAudioDevice::GetCurrentPhysicalFormats(bool inIsInput, UInt32& ioNumberStreams, AudioStreamBasicDescription* outFormats) const { ioNumberStreams = std::min(ioNumberStreams, GetNumberStreams(inIsInput)); for(UInt32 theIndex = 0; theIndex < ioNumberStreams; ++theIndex) { CAHALAudioStream theStream(GetStreamByIndex(inIsInput, theIndex)); theStream.GetCurrentPhysicalFormat(outFormats[theIndex]); } } bool CAHALAudioDevice::IsRunning() const { CAPropertyAddress theAddress(kAudioDevicePropertyDeviceIsRunning); UInt32 theAnswer = GetPropertyData_UInt32(theAddress, 0, NULL); return theAnswer != 0; } bool CAHALAudioDevice::IsRunningSomewhere() const { CAPropertyAddress theAddress(kAudioDevicePropertyDeviceIsRunningSomewhere); UInt32 theAnswer = 0; if(HasProperty(theAddress)) { theAnswer = GetPropertyData_UInt32(theAddress, 0, NULL); } return theAnswer != 0; } UInt32 CAHALAudioDevice::GetLatency(bool inIsInput) const { CAPropertyAddress theAddress(kAudioDevicePropertyLatency, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); return GetPropertyData_UInt32(theAddress, 0, NULL); } UInt32 CAHALAudioDevice::GetSafetyOffset(bool inIsInput) const { CAPropertyAddress theAddress(kAudioDevicePropertySafetyOffset, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); return GetPropertyData_UInt32(theAddress, 0, NULL); } bool CAHALAudioDevice::HasClockDomain() const { CAPropertyAddress theAddress(kAudioDevicePropertyClockDomain); return HasProperty(theAddress); } UInt32 CAHALAudioDevice::GetClockDomain() const { CAPropertyAddress theAddress(kAudioDevicePropertyClockDomain); return GetPropertyData_UInt32(theAddress, 0, NULL); } Float64 CAHALAudioDevice::GetActualSampleRate() const { Float64 theAnswer = 0; CAPropertyAddress theAddress(kAudioDevicePropertyActualSampleRate); if(HasProperty(theAddress)) { UInt32 theSize = sizeof(Float64); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } else { theAnswer = GetNominalSampleRate(); } return theAnswer; } Float64 CAHALAudioDevice::GetNominalSampleRate() const { Float64 theAnswer = 0; CAPropertyAddress theAddress(kAudioDevicePropertyNominalSampleRate); UInt32 theSize = sizeof(Float64); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } void CAHALAudioDevice::SetNominalSampleRate(Float64 inSampleRate) { CAPropertyAddress theAddress(kAudioDevicePropertyNominalSampleRate); SetPropertyData(theAddress, 0, NULL, sizeof(Float64), &inSampleRate); } UInt32 CAHALAudioDevice::GetNumberAvailableNominalSampleRateRanges() const { UInt32 theAnswer = 0; CAPropertyAddress theAddress(kAudioDevicePropertyAvailableNominalSampleRates); if(HasProperty(theAddress)) { UInt32 theSize = GetPropertyDataSize(theAddress, 0, NULL); theAnswer = theSize / SizeOf32(AudioValueRange); } return theAnswer; } void CAHALAudioDevice::GetAvailableNominalSampleRateRanges(UInt32& ioNumberRanges, AudioValueRange* outRanges) const { CAPropertyAddress theAddress(kAudioDevicePropertyAvailableNominalSampleRates); if(HasProperty(theAddress)) { UInt32 theSize = ioNumberRanges * SizeOf32(AudioValueRange); GetPropertyData(theAddress, 0, NULL, theSize, outRanges); ioNumberRanges = theSize / SizeOf32(AudioValueRange); } else { ioNumberRanges = 0; } } void CAHALAudioDevice::GetAvailableNominalSampleRateRangeByIndex(UInt32 inIndex, Float64& outMinimum, Float64& outMaximum) const { UInt32 theNumberRanges = GetNumberAvailableNominalSampleRateRanges(); ThrowIf(inIndex >= theNumberRanges, CAException(kAudioHardwareIllegalOperationError), "CAHALAudioDevice::GetAvailableNominalSampleRateRangeByIndex: index out of range"); CAAutoArrayDelete theRanges(theNumberRanges); GetAvailableNominalSampleRateRanges(theNumberRanges, theRanges); outMinimum = theRanges[inIndex].mMinimum; outMaximum = theRanges[inIndex].mMaximum; } bool CAHALAudioDevice::IsValidNominalSampleRate(Float64 inSampleRate) const { bool theAnswer = false; UInt32 theNumberRanges = GetNumberAvailableNominalSampleRateRanges(); CAAutoArrayDelete theRanges(theNumberRanges); GetAvailableNominalSampleRateRanges(theNumberRanges, theRanges); for(UInt32 theIndex = 0; !theAnswer && (theIndex < theNumberRanges); ++theIndex) { theAnswer = (inSampleRate >= theRanges[theIndex].mMinimum) && (inSampleRate <= theRanges[theIndex].mMinimum); } return theAnswer; } bool CAHALAudioDevice::IsIOBufferSizeSettable() const { CAPropertyAddress theAddress(kAudioDevicePropertyBufferFrameSize); return IsPropertySettable(theAddress); } UInt32 CAHALAudioDevice::GetIOBufferSize() const { CAPropertyAddress theAddress(kAudioDevicePropertyBufferFrameSize); return GetPropertyData_UInt32(theAddress, 0, NULL); } void CAHALAudioDevice::SetIOBufferSize(UInt32 inBufferSize) { CAPropertyAddress theAddress(kAudioDevicePropertyBufferFrameSize); SetPropertyData(theAddress, 0, NULL, sizeof(UInt32), &inBufferSize); } bool CAHALAudioDevice::UsesVariableIOBufferSizes() const { CAPropertyAddress theAddress(kAudioDevicePropertyUsesVariableBufferFrameSizes); return HasProperty(theAddress); } UInt32 CAHALAudioDevice::GetMaximumVariableIOBufferSize() const { CAPropertyAddress theAddress(kAudioDevicePropertyUsesVariableBufferFrameSizes); return GetPropertyData_UInt32(theAddress, 0, NULL); } bool CAHALAudioDevice::HasIOBufferSizeRange() const { CAPropertyAddress theAddress(kAudioDevicePropertyBufferFrameSizeRange); return HasProperty(theAddress); } void CAHALAudioDevice::GetIOBufferSizeRange(UInt32& outMinimum, UInt32& outMaximum) const { AudioValueRange theAnswer = { 0, 0 }; CAPropertyAddress theAddress(kAudioDevicePropertyBufferFrameSizeRange); UInt32 theSize = sizeof(AudioValueRange); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); outMinimum = static_cast(theAnswer.mMinimum); outMaximum = static_cast(theAnswer.mMaximum); } AudioDeviceIOProcID CAHALAudioDevice::CreateIOProcID(AudioDeviceIOProc inIOProc, void* inClientData) { AudioDeviceIOProcID theAnswer = NULL; OSStatus theError = AudioDeviceCreateIOProcID(mObjectID, inIOProc, inClientData, &theAnswer); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::CreateIOProcID: got an error creating the IOProc ID"); return theAnswer; } void CAHALAudioDevice::DestroyIOProcID(AudioDeviceIOProcID inIOProcID) { OSStatus theError = AudioDeviceDestroyIOProcID(mObjectID, inIOProcID); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::DestroyIOProcID: got an error destroying the IOProc ID"); } void CAHALAudioDevice::StartIOProc(AudioDeviceIOProcID inIOProcID) { OSStatus theError = AudioDeviceStart(mObjectID, inIOProcID); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::StartIOProc: got an error starting an IOProc"); } void CAHALAudioDevice::StartIOProcAtTime(AudioDeviceIOProcID inIOProcID, AudioTimeStamp& ioStartTime, bool inIsInput, bool inIgnoreHardware) { UInt32 theFlags = 0; if(inIsInput) { theFlags |= kAudioDeviceStartTimeIsInputFlag; } if(inIgnoreHardware) { theFlags |= kAudioDeviceStartTimeDontConsultDeviceFlag; } OSStatus theError = AudioDeviceStartAtTime(mObjectID, inIOProcID, &ioStartTime, theFlags); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::StartIOProcAtTime: got an error starting an IOProc"); } void CAHALAudioDevice::StopIOProc(AudioDeviceIOProcID inIOProcID) { OSStatus theError = AudioDeviceStop(mObjectID, inIOProcID); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::StopIOProc: got an error stopping an IOProc"); } void CAHALAudioDevice::GetIOProcStreamUsage(AudioDeviceIOProcID inIOProcID, bool inIsInput, bool* outStreamUsage) const { // make an AudioHardwareIOProcStreamUsage the right size UInt32 theNumberStreams = GetNumberStreams(inIsInput); UInt32 theSize = SizeOf32(void*) + SizeOf32(UInt32) + (theNumberStreams * SizeOf32(UInt32)); CAAutoFree theStreamUsage(theSize); // set it up theStreamUsage->mIOProc = reinterpret_cast(inIOProcID); theStreamUsage->mNumberStreams = theNumberStreams; // get the property CAPropertyAddress theAddress(kAudioDevicePropertyIOProcStreamUsage, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); GetPropertyData(theAddress, 0, NULL, theSize, theStreamUsage); // fill out the return value for(UInt32 theIndex = 0; theIndex < theNumberStreams; ++theIndex) { outStreamUsage[theIndex] = (theStreamUsage->mStreamIsOn[theIndex] != 0); } } void CAHALAudioDevice::SetIOProcStreamUsage(AudioDeviceIOProcID inIOProcID, bool inIsInput, const bool* inStreamUsage) { // make an AudioHardwareIOProcStreamUsage the right size UInt32 theNumberStreams = GetNumberStreams(inIsInput); UInt32 theSize = SizeOf32(void*) + SizeOf32(UInt32) + (theNumberStreams * SizeOf32(UInt32)); CAAutoFree theStreamUsage(theSize); // set it up theStreamUsage->mIOProc = reinterpret_cast(inIOProcID); theStreamUsage->mNumberStreams = theNumberStreams; for(UInt32 theIndex = 0; theIndex < theNumberStreams; ++theIndex) { theStreamUsage->mStreamIsOn[theIndex] = (inStreamUsage[theIndex] ? 1 : 0); } // set the property CAPropertyAddress theAddress(kAudioDevicePropertyIOProcStreamUsage, inIsInput ? kAudioDevicePropertyScopeInput : kAudioDevicePropertyScopeOutput); SetPropertyData(theAddress, 0, NULL, theSize, theStreamUsage); } Float32 CAHALAudioDevice::GetIOCycleUsage() const { Float32 theAnswer = 0; CAPropertyAddress theAddress(kAudioDevicePropertyIOCycleUsage); UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } void CAHALAudioDevice::SetIOCycleUsage(Float32 inValue) { CAPropertyAddress theAddress(kAudioDevicePropertyIOCycleUsage); SetPropertyData(theAddress, 0, NULL, sizeof(Float32), &inValue); } void CAHALAudioDevice::GetCurrentTime(AudioTimeStamp& outTime) { OSStatus theError = AudioDeviceGetCurrentTime(mObjectID, &outTime); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::GetCurrentTime: got an error getting the current time"); } void CAHALAudioDevice::TranslateTime(const AudioTimeStamp& inTime, AudioTimeStamp& outTime) { OSStatus theError = AudioDeviceTranslateTime(mObjectID, &inTime, &outTime); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::TranslateTime: got an error translating time"); } void CAHALAudioDevice::GetNearestStartTime(AudioTimeStamp& ioTime, bool inIsInput, bool inIgnoreHardware) { UInt32 theFlags = 0; if(inIsInput) { theFlags |= kAudioDeviceStartTimeIsInputFlag; } if(inIgnoreHardware) { theFlags |= kAudioDeviceStartTimeDontConsultDeviceFlag; } OSStatus theError = AudioDeviceGetNearestStartTime(mObjectID, &ioTime, theFlags); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::GetNearestStartTime: got an error getting the start time"); } bool CAHALAudioDevice::HasVolumeControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeScalar, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::VolumeControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeScalar, inScope, inChannel); return IsPropertySettable(theAddress); } Float32 CAHALAudioDevice::GetVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeScalar, inScope, inChannel); Float32 theValue = 0.0f; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } Float32 CAHALAudioDevice::GetVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeDecibels, inScope, inChannel); Float32 theValue = 0.0f; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } void CAHALAudioDevice::SetVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeScalar, inScope, inChannel); SetPropertyData(theAddress, 0, NULL, sizeof(Float32), &inValue); } void CAHALAudioDevice::SetVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeDecibels, inScope, inChannel); SetPropertyData(theAddress, 0, NULL, sizeof(Float32), &inValue); } Float32 CAHALAudioDevice::GetVolumeControlScalarForDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeDecibelsToScalar, inScope, inChannel); Float32 theValue = inValue; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } Float32 CAHALAudioDevice::GetVolumeControlDecibelForScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { CAPropertyAddress theAddress(kAudioDevicePropertyVolumeScalarToDecibels, inScope, inChannel); Float32 theValue = inValue; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } bool CAHALAudioDevice::HasSubVolumeControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeScalar, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::SubVolumeControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeScalar, inScope, inChannel); return IsPropertySettable(theAddress); } Float32 CAHALAudioDevice::GetSubVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeScalar, inScope, inChannel); Float32 theValue = 0.0f; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } Float32 CAHALAudioDevice::GetSubVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeDecibels, inScope, inChannel); Float32 theValue = 0.0f; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } void CAHALAudioDevice::SetSubVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeScalar, inScope, inChannel); SetPropertyData(theAddress, 0, NULL, sizeof(Float32), &inValue); } void CAHALAudioDevice::SetSubVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeDecibels, inScope, inChannel); SetPropertyData(theAddress, 0, NULL, sizeof(Float32), &inValue); } Float32 CAHALAudioDevice::GetSubVolumeControlScalarForDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeDecibelsToScalar, inScope, inChannel); Float32 theValue = inValue; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } Float32 CAHALAudioDevice::GetSubVolumeControlDecibelForScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const { CAPropertyAddress theAddress(kAudioDevicePropertySubVolumeScalarToDecibels, inScope, inChannel); Float32 theValue = inValue; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } bool CAHALAudioDevice::HasMuteControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyMute, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::MuteControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyMute, inScope, inChannel); return IsPropertySettable(theAddress); } bool CAHALAudioDevice::GetMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyMute, inScope, inChannel); UInt32 theValue = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue != 0; } void CAHALAudioDevice::SetMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { CAPropertyAddress theAddress(kAudioDevicePropertyMute, inScope, inChannel); UInt32 theValue = (inValue ? 1 : 0); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &theValue); } bool CAHALAudioDevice::HasSoloControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySolo, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::SoloControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySolo, inScope, inChannel); return IsPropertySettable(theAddress); } bool CAHALAudioDevice::GetSoloControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySolo, inScope, inChannel); UInt32 theValue = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue != 0; } void CAHALAudioDevice::SetSoloControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { CAPropertyAddress theAddress(kAudioDevicePropertySolo, inScope, inChannel); UInt32 theValue = (inValue ? 1 : 0); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &theValue); } bool CAHALAudioDevice::HasStereoPanControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyStereoPan, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::StereoPanControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyStereoPan, inScope, inChannel); return IsPropertySettable(theAddress); } Float32 CAHALAudioDevice::GetStereoPanControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyStereoPan, inScope, inChannel); Float32 theValue = 0.0f; UInt32 theSize = sizeof(Float32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue; } void CAHALAudioDevice::SetStereoPanControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) { CAPropertyAddress theAddress(kAudioDevicePropertyStereoPan, inScope, inChannel); UInt32 theSize = sizeof(Float32); SetPropertyData(theAddress, 0, NULL, theSize, &inValue); } void CAHALAudioDevice::GetStereoPanControlChannels(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& outLeftChannel, UInt32& outRightChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyStereoPanChannels, inScope, inChannel); UInt32 theValue[2] = { 0, 0 }; UInt32 theSize = 2 * sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, theValue); outLeftChannel = theValue[0]; outRightChannel = theValue[1]; } bool CAHALAudioDevice::HasJackControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyJackIsConnected, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::GetJackControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyJackIsConnected, inScope, inChannel); UInt32 theValue = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue != 0; } bool CAHALAudioDevice::HasSubMuteControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubMute, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::SubMuteControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubMute, inScope, inChannel); return IsPropertySettable(theAddress); } bool CAHALAudioDevice::GetSubMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertySubMute, inScope, inChannel); UInt32 theValue = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue != 0; } void CAHALAudioDevice::SetSubMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { CAPropertyAddress theAddress(kAudioDevicePropertySubMute, inScope, inChannel); UInt32 theValue = (inValue ? 1 : 0); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &theValue); } bool CAHALAudioDevice::HasiSubOwnerControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDriverShouldOwniSub, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::iSubOwnerControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDriverShouldOwniSub, inScope, inChannel); return IsPropertySettable(theAddress); } bool CAHALAudioDevice::GetiSubOwnerControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDriverShouldOwniSub, inScope, inChannel); UInt32 theValue = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theValue != 0; } void CAHALAudioDevice::SetiSubOwnerControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue) { CAPropertyAddress theAddress(kAudioDevicePropertyDriverShouldOwniSub, inScope, inChannel); UInt32 theValue = (inValue ? 1 : 0); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &theValue); } bool CAHALAudioDevice::HasDataSourceControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDataSource, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::DataSourceControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDataSource, inScope, inChannel); return IsPropertySettable(theAddress); } UInt32 CAHALAudioDevice::GetCurrentDataSourceID(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDataSource, inScope, inChannel); UInt32 theAnswer = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } void CAHALAudioDevice::SetCurrentDataSourceByID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) { CAPropertyAddress theAddress(kAudioDevicePropertyDataSource, inScope, inChannel); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &inID); } UInt32 CAHALAudioDevice::GetNumberAvailableDataSources(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyDataSources, inScope, inChannel); UInt32 theAnswer = 0; if(HasProperty(theAddress)) { UInt32 theSize = GetPropertyDataSize(theAddress, 0, NULL); theAnswer = theSize / SizeOf32(UInt32); } return theAnswer; } void CAHALAudioDevice::GetAvailableDataSources(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& ioNumberSources, UInt32* outSources) const { CAPropertyAddress theAddress(kAudioDevicePropertyDataSources, inScope, inChannel); UInt32 theNumberSources = std::min(GetNumberAvailableDataSources(inScope, inChannel), ioNumberSources); UInt32 theSize = theNumberSources * SizeOf32(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, outSources); ioNumberSources = theSize / SizeOf32(UInt32); } UInt32 CAHALAudioDevice::GetAvailableDataSourceByIndex(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inIndex) const { AudioStreamID theAnswer = 0; UInt32 theNumberSources = GetNumberAvailableDataSources(inScope, inChannel); if((theNumberSources > 0) && (inIndex < theNumberSources)) { CAAutoArrayDelete theSourceList(theNumberSources); GetAvailableDataSources(inScope, inChannel, theNumberSources, theSourceList); theAnswer = theSourceList[inIndex]; } return theAnswer; } CFStringRef CAHALAudioDevice::CopyDataSourceNameForID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) const { CAPropertyAddress theAddress(kAudioDevicePropertyDataSourceNameForIDCFString, inScope, inChannel); CFStringRef theAnswer = NULL; AudioValueTranslation theTranslation = { &inID, sizeof(UInt32), &theAnswer, sizeof(CFStringRef) }; UInt32 theSize = sizeof(AudioValueTranslation); GetPropertyData(theAddress, 0, NULL, theSize, &theTranslation); return theAnswer; } bool CAHALAudioDevice::HasDataDestinationControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestination, inScope, inChannel); return HasProperty(theAddress); } bool CAHALAudioDevice::DataDestinationControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestination, inScope, inChannel); return IsPropertySettable(theAddress); } UInt32 CAHALAudioDevice::GetCurrentDataDestinationID(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestination, inScope, inChannel); UInt32 theAnswer = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } void CAHALAudioDevice::SetCurrentDataDestinationByID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestination, inScope, inChannel); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &inID); } UInt32 CAHALAudioDevice::GetNumberAvailableDataDestinations(AudioObjectPropertyScope inScope, UInt32 inChannel) const { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestinations, inScope, inChannel); UInt32 theAnswer = 0; if(HasProperty(theAddress)) { UInt32 theSize = GetPropertyDataSize(theAddress, 0, NULL); theAnswer = theSize / SizeOf32(UInt32); } return theAnswer; } void CAHALAudioDevice::GetAvailableDataDestinations(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& ioNumberDestinations, UInt32* outDestinations) const { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestinations, inScope, inChannel); UInt32 theNumberDestinations = std::min(GetNumberAvailableDataDestinations(inScope, inChannel), ioNumberDestinations); UInt32 theSize = theNumberDestinations * SizeOf32(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, outDestinations); ioNumberDestinations = theSize / SizeOf32(UInt32); } UInt32 CAHALAudioDevice::GetAvailableDataDestinationByIndex(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inIndex) const { AudioStreamID theAnswer = 0; UInt32 theNumberDestinations = GetNumberAvailableDataDestinations(inScope, inChannel); if((theNumberDestinations > 0) && (inIndex < theNumberDestinations)) { CAAutoArrayDelete theDestinationList(theNumberDestinations); GetAvailableDataDestinations(inScope, inChannel, theNumberDestinations, theDestinationList); theAnswer = theDestinationList[inIndex]; } return theAnswer; } CFStringRef CAHALAudioDevice::CopyDataDestinationNameForID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) const { CAPropertyAddress theAddress(kAudioDevicePropertyPlayThruDestinationNameForIDCFString, inScope, inChannel); CFStringRef theAnswer = NULL; AudioValueTranslation theTranslation = { &inID, sizeof(UInt32), &theAnswer, sizeof(CFStringRef) }; UInt32 theSize = sizeof(AudioValueTranslation); GetPropertyData(theAddress, 0, NULL, theSize, &theTranslation); return theAnswer; } bool CAHALAudioDevice::HasClockSourceControl() const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSource); return HasProperty(theAddress); } bool CAHALAudioDevice::ClockSourceControlIsSettable() const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSource); return IsPropertySettable(theAddress); } UInt32 CAHALAudioDevice::GetCurrentClockSourceID() const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSource); UInt32 theAnswer = 0; UInt32 theSize = sizeof(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } void CAHALAudioDevice::SetCurrentClockSourceByID(UInt32 inID) { CAPropertyAddress theAddress(kAudioDevicePropertyClockSource); UInt32 theSize = sizeof(UInt32); SetPropertyData(theAddress, 0, NULL, theSize, &inID); } UInt32 CAHALAudioDevice::GetNumberAvailableClockSources() const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSources); UInt32 theAnswer = 0; if(HasProperty(theAddress)) { UInt32 theSize = GetPropertyDataSize(theAddress, 0, NULL); theAnswer = theSize / SizeOf32(UInt32); } return theAnswer; } void CAHALAudioDevice::GetAvailableClockSources(UInt32& ioNumberSources, UInt32* outSources) const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSources); UInt32 theNumberSources = std::min(GetNumberAvailableClockSources(), ioNumberSources); UInt32 theSize = theNumberSources * SizeOf32(UInt32); GetPropertyData(theAddress, 0, NULL, theSize, outSources); ioNumberSources = theSize / SizeOf32(UInt32); } UInt32 CAHALAudioDevice::GetAvailableClockSourceByIndex(UInt32 inIndex) const { AudioStreamID theAnswer = 0; UInt32 theNumberSources = GetNumberAvailableClockSources(); if((theNumberSources > 0) && (inIndex < theNumberSources)) { CAAutoArrayDelete theSourceList(theNumberSources); GetAvailableClockSources(theNumberSources, theSourceList); theAnswer = theSourceList[inIndex]; } return theAnswer; } CFStringRef CAHALAudioDevice::CopyClockSourceNameForID(UInt32 inID) const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSourceNameForIDCFString); CFStringRef theAnswer = NULL; AudioValueTranslation theTranslation = { &inID, sizeof(UInt32), &theAnswer, sizeof(CFStringRef) }; UInt32 theSize = sizeof(AudioValueTranslation); GetPropertyData(theAddress, 0, NULL, theSize, &theTranslation); return theAnswer; } UInt32 CAHALAudioDevice::GetClockSourceKindForID(UInt32 inID) const { CAPropertyAddress theAddress(kAudioDevicePropertyClockSourceKindForID); UInt32 theAnswer = 0; AudioValueTranslation theTranslation = { &inID, sizeof(UInt32), &theAnswer, sizeof(UInt32) }; UInt32 theSize = sizeof(AudioValueTranslation); GetPropertyData(theAddress, 0, NULL, theSize, &theTranslation); return theAnswer; } ================================================ FILE: BGMApp/PublicUtility/CAHALAudioDevice.h ================================================ /* File: CAHALAudioDevice.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAHALAudioDevice_h__) #define __CAHALAudioDevice_h__ //================================================================================================== // Includes //================================================================================================== // Super Class Includes #include "CAHALAudioObject.h" // PublicUtility Includes #include "CADebugMacros.h" #include "CAException.h" //================================================================================================== // CAHALAudioDevice //================================================================================================== class CAHALAudioDevice : public CAHALAudioObject { // Construction/Destruction public: CAHALAudioDevice(AudioObjectID inAudioDevice); CAHALAudioDevice(CFStringRef inUID); virtual ~CAHALAudioDevice(); // General Stuff public: CFStringRef CopyDeviceUID() const; bool HasModelUID() const; CFStringRef CopyModelUID() const; CFStringRef CopyConfigurationApplicationBundleID() const; CFURLRef CopyIconLocation() const; UInt32 GetTransportType() const; bool CanBeDefaultDevice(bool inIsInput, bool inIsSystem) const; bool HasDevicePlugInStatus() const; OSStatus GetDevicePlugInStatus() const; bool IsAlive() const; bool IsHidden() const; pid_t GetHogModeOwner() const; bool IsHogModeSettable() const; bool TakeHogMode(); void ReleaseHogMode(); bool HasPreferredStereoChannels(bool inIsInput) const; void GetPreferredStereoChannels(bool inIsInput, UInt32& outLeft, UInt32& outRight) const; void SetPreferredStereoChannels(bool inIsInput, UInt32 inLeft, UInt32 inRight); bool HasPreferredChannelLayout(bool inIsInput) const; void GetPreferredChannelLayout(bool inIsInput, AudioChannelLayout& outChannelLayout) const; void SetPreferredStereoChannels(bool inIsInput, AudioChannelLayout& inChannelLayout); UInt32 GetNumberRelatedAudioDevices() const; void GetRelatedAudioDevices(UInt32& ioNumberRelatedDevices, AudioObjectID* outRelatedDevices) const; AudioObjectID GetRelatedAudioDeviceByIndex(UInt32 inIndex) const; // Stream Stuff public: UInt32 GetNumberStreams(bool inIsInput) const; void GetStreams(bool inIsInput, UInt32& ioNumberStreams, AudioObjectID* outStreamList) const; AudioObjectID GetStreamByIndex(bool inIsInput, UInt32 inIndex) const; UInt32 GetTotalNumberChannels(bool inIsInput) const; void GetCurrentVirtualFormats(bool inIsInput, UInt32& ioNumberStreams, AudioStreamBasicDescription* outFormats) const; void GetCurrentPhysicalFormats(bool inIsInput, UInt32& ioNumberStreams, AudioStreamBasicDescription* outFormats) const; // IO Stuff public: bool IsRunning() const; bool IsRunningSomewhere() const; UInt32 GetLatency(bool inIsInput) const; UInt32 GetSafetyOffset(bool inIsInput) const; bool HasClockDomain() const; UInt32 GetClockDomain() const; Float64 GetActualSampleRate() const; Float64 GetNominalSampleRate() const; void SetNominalSampleRate(Float64 inSampleRate); UInt32 GetNumberAvailableNominalSampleRateRanges() const; void GetAvailableNominalSampleRateRanges(UInt32& ioNumberRanges, AudioValueRange* outRanges) const; void GetAvailableNominalSampleRateRangeByIndex(UInt32 inIndex, Float64& outMinimum, Float64& outMaximum) const; bool IsValidNominalSampleRate(Float64 inSampleRate) const; bool IsIOBufferSizeSettable() const; UInt32 GetIOBufferSize() const; void SetIOBufferSize(UInt32 inBufferSize); bool UsesVariableIOBufferSizes() const; UInt32 GetMaximumVariableIOBufferSize() const; bool HasIOBufferSizeRange() const; void GetIOBufferSizeRange(UInt32& outMinimum, UInt32& outMaximum) const; AudioDeviceIOProcID CreateIOProcID(AudioDeviceIOProc inIOProc, void* inClientData); AudioDeviceIOProcID CreateIOProcIDWithBlock(dispatch_queue_t inDispatchQueue, AudioDeviceIOBlock inIOBlock); void DestroyIOProcID(AudioDeviceIOProcID inIOProcID); void StartIOProc(AudioDeviceIOProcID inIOProcID); void StartIOProcAtTime(AudioDeviceIOProcID inIOProcID, AudioTimeStamp& ioStartTime, bool inIsInput, bool inIgnoreHardware); void StopIOProc(AudioDeviceIOProcID inIOProcID); void GetIOProcStreamUsage(AudioDeviceIOProcID inIOProcID, bool inIsInput, bool* outStreamUsage) const; void SetIOProcStreamUsage(AudioDeviceIOProcID inIOProcID, bool inIsInput, const bool* inStreamUsage); Float32 GetIOCycleUsage() const; void SetIOCycleUsage(Float32 inValue); // Time Operations public: void GetCurrentTime(AudioTimeStamp& outTime); void TranslateTime(const AudioTimeStamp& inTime, AudioTimeStamp& outTime); void GetNearestStartTime(AudioTimeStamp& ioTime, bool inIsInput, bool inIgnoreHardware); // Controls public: bool HasVolumeControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool VolumeControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; Float32 GetVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; Float32 GetVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue); void SetVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue); Float32 GetVolumeControlScalarForDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const; Float32 GetVolumeControlDecibelForScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const; bool HasSubVolumeControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool SubVolumeControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; Float32 GetSubVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; Float32 GetSubVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetSubVolumeControlScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue); void SetSubVolumeControlDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue); Float32 GetSubVolumeControlScalarForDecibelValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const; Float32 GetSubVolumeControlDecibelForScalarValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue) const; bool HasMuteControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool MuteControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool GetMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue); bool HasSoloControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool SoloControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool GetSoloControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetSoloControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue); bool HasStereoPanControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool StereoPanControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; Float32 GetStereoPanControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetStereoPanControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, Float32 inValue); void GetStereoPanControlChannels(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& outLeftChannel, UInt32& outRightChannel) const; bool HasJackControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool GetJackControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool HasSubMuteControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool SubMuteControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool GetSubMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetSubMuteControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue); bool HasiSubOwnerControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool iSubOwnerControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool GetiSubOwnerControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetiSubOwnerControlValue(AudioObjectPropertyScope inScope, UInt32 inChannel, bool inValue); bool HasDataSourceControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool DataSourceControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; UInt32 GetCurrentDataSourceID(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetCurrentDataSourceByID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID); UInt32 GetNumberAvailableDataSources(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void GetAvailableDataSources(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& ioNumberSources, UInt32* outSources) const; UInt32 GetAvailableDataSourceByIndex(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inIndex) const; CFStringRef CopyDataSourceNameForID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) const; bool HasDataDestinationControl(AudioObjectPropertyScope inScope, UInt32 inChannel) const; bool DataDestinationControlIsSettable(AudioObjectPropertyScope inScope, UInt32 inChannel) const; UInt32 GetCurrentDataDestinationID(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void SetCurrentDataDestinationByID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID); UInt32 GetNumberAvailableDataDestinations(AudioObjectPropertyScope inScope, UInt32 inChannel) const; void GetAvailableDataDestinations(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32& ioNumberDestinations, UInt32* outDestinations) const; UInt32 GetAvailableDataDestinationByIndex(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inIndex) const; CFStringRef CopyDataDestinationNameForID(AudioObjectPropertyScope inScope, UInt32 inChannel, UInt32 inID) const; bool HasClockSourceControl() const; bool ClockSourceControlIsSettable() const; UInt32 GetCurrentClockSourceID() const; void SetCurrentClockSourceByID(UInt32 inID); UInt32 GetNumberAvailableClockSources() const; void GetAvailableClockSources(UInt32& ioNumberSources, UInt32* outSources) const; UInt32 GetAvailableClockSourceByIndex(UInt32 inIndex) const; CFStringRef CopyClockSourceNameForID(UInt32 inID) const; UInt32 GetClockSourceKindForID(UInt32 inID) const; }; inline AudioDeviceIOProcID CAHALAudioDevice::CreateIOProcIDWithBlock(dispatch_queue_t inDispatchQueue, AudioDeviceIOBlock inIOBlock) { AudioDeviceIOProcID theAnswer = NULL; OSStatus theError = AudioDeviceCreateIOProcIDWithBlock(&theAnswer, mObjectID, inDispatchQueue, inIOBlock); ThrowIfError(theError, CAException(theError), "CAHALAudioDevice::CreateIOProcIDWithBlock: got an error creating the IOProc ID"); return theAnswer; } #endif ================================================ FILE: BGMApp/PublicUtility/CAHALAudioObject.cpp ================================================ /* File: CAHALAudioObject.cpp Abstract: CAHALAudioObject.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //================================================================================================== // Includes //================================================================================================== // Self Include #include "CAHALAudioObject.h" // PublicUtility Includes #include "CAAutoDisposer.h" #include "CADebugMacros.h" #include "CAException.h" #include "CAPropertyAddress.h" //================================================================================================== // CAHALAudioObject //================================================================================================== CAHALAudioObject::CAHALAudioObject(AudioObjectID inObjectID) : mObjectID(inObjectID) { } CAHALAudioObject::~CAHALAudioObject() { } AudioObjectID CAHALAudioObject::GetObjectID() const { return mObjectID; } void CAHALAudioObject::SetObjectID(AudioObjectID inObjectID) { mObjectID = inObjectID; } AudioClassID CAHALAudioObject::GetClassID() const { // set up the return value AudioClassID theAnswer = 0; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyClass); // make sure the property exists if(HasProperty(theAddress)) { UInt32 theSize = sizeof(AudioClassID); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } AudioObjectID CAHALAudioObject::GetOwnerObjectID() const { // set up the return value AudioObjectID theAnswer = 0; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyOwner); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(AudioObjectID); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } CFStringRef CAHALAudioObject::CopyOwningPlugInBundleID() const { // set up the return value CFStringRef theAnswer = NULL; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyCreator); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(CFStringRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } CFStringRef CAHALAudioObject::CopyName() const { // set up the return value CFStringRef theAnswer = NULL; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyName); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(CFStringRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } CFStringRef CAHALAudioObject::CopyManufacturer() const { // set up the return value CFStringRef theAnswer = NULL; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyManufacturer); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(CFStringRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } CFStringRef CAHALAudioObject::CopyNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const { // set up the return value CFStringRef theAnswer = NULL; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyElementName, inScope, inElement); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(CFStringRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } CFStringRef CAHALAudioObject::CopyCategoryNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const { // set up the return value CFStringRef theAnswer = NULL; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyElementCategoryName, inScope, inElement); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(CFStringRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } CFStringRef CAHALAudioObject::CopyNumberNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const { // set up the return value CFStringRef theAnswer = NULL; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyElementNumberName, inScope, inElement); // make sure the property exists if(HasProperty(theAddress)) { // get the property data UInt32 theSize = sizeof(CFStringRef); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); } return theAnswer; } bool CAHALAudioObject::ObjectExists(AudioObjectID inObjectID) { Boolean isSettable; CAPropertyAddress theAddress(kAudioObjectPropertyClass); // BGM edit: Negated the expression returned. Seems to have been a bug. //return (inObjectID == 0) || (AudioObjectIsPropertySettable(inObjectID, &theAddress, &isSettable) != 0); return (inObjectID != kAudioObjectUnknown) && (AudioObjectIsPropertySettable(inObjectID, &theAddress, &isSettable) == kAudioHardwareNoError); // BGM edit end } UInt32 CAHALAudioObject::GetNumberOwnedObjects(AudioClassID inClass) const { // set up the return value UInt32 theAnswer = 0; // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyOwnedObjects); // figure out the qualifier UInt32 theQualifierSize = 0; void* theQualifierData = NULL; if(inClass != 0) { theQualifierSize = sizeof(AudioObjectID); theQualifierData = &inClass; } // get the property data size theAnswer = GetPropertyDataSize(theAddress, theQualifierSize, theQualifierData); // calculate the number of object IDs theAnswer /= SizeOf32(AudioObjectID); return theAnswer; } void CAHALAudioObject::GetAllOwnedObjects(AudioClassID inClass, UInt32& ioNumberObjects, AudioObjectID* ioObjectIDs) const { // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyOwnedObjects); // figure out the qualifier UInt32 theQualifierSize = 0; void* theQualifierData = NULL; if(inClass != 0) { theQualifierSize = sizeof(AudioObjectID); theQualifierData = &inClass; } // get the property data UInt32 theDataSize = ioNumberObjects * SizeOf32(AudioClassID); GetPropertyData(theAddress, theQualifierSize, theQualifierData, theDataSize, ioObjectIDs); // set the number of object IDs being returned ioNumberObjects = theDataSize / SizeOf32(AudioObjectID); } AudioObjectID CAHALAudioObject::GetOwnedObjectByIndex(AudioClassID inClass, UInt32 inIndex) { // set up the property address CAPropertyAddress theAddress(kAudioObjectPropertyOwnedObjects); // figure out the qualifier UInt32 theQualifierSize = 0; void* theQualifierData = NULL; if(inClass != 0) { theQualifierSize = sizeof(AudioObjectID); theQualifierData = &inClass; } // figure out how much space to allocate UInt32 theDataSize = GetPropertyDataSize(theAddress, theQualifierSize, theQualifierData); UInt32 theNumberObjectIDs = theDataSize / SizeOf32(AudioObjectID); // set up the return value AudioObjectID theAnswer = 0; // maker sure the index is in range if(inIndex < theNumberObjectIDs) { // allocate it CAAutoArrayDelete theObjectList(theDataSize / sizeof(AudioObjectID)); // get the property data GetPropertyData(theAddress, theQualifierSize, theQualifierData, theDataSize, theObjectList); // get the return value theAnswer = theObjectList[inIndex]; } return theAnswer; } bool CAHALAudioObject::HasProperty(const AudioObjectPropertyAddress& inAddress) const { return AudioObjectHasProperty(mObjectID, &inAddress); } bool CAHALAudioObject::IsPropertySettable(const AudioObjectPropertyAddress& inAddress) const { Boolean isSettable = false; OSStatus theError = AudioObjectIsPropertySettable(mObjectID, &inAddress, &isSettable); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::IsPropertySettable: got an error getting info about a property"); return isSettable != 0; } UInt32 CAHALAudioObject::GetPropertyDataSize(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData) const { UInt32 theDataSize = 0; OSStatus theError = AudioObjectGetPropertyDataSize(mObjectID, &inAddress, inQualifierDataSize, inQualifierData, &theDataSize); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::GetPropertyDataSize: got an error getting the property data size"); return theDataSize; } void CAHALAudioObject::GetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32& ioDataSize, void* outData) const { OSStatus theError = AudioObjectGetPropertyData(mObjectID, &inAddress, inQualifierDataSize, inQualifierData, &ioDataSize, outData); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::GetPropertyData: got an error getting the property data"); } void CAHALAudioObject::SetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData) { OSStatus theError = AudioObjectSetPropertyData(mObjectID, &inAddress, inQualifierDataSize, inQualifierData, inDataSize, inData); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::SetPropertyData: got an error setting the property data"); } void CAHALAudioObject::AddPropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData) { OSStatus theError = AudioObjectAddPropertyListener(mObjectID, &inAddress, inListenerProc, inClientData); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::AddPropertyListener: got an error adding a property listener"); } void CAHALAudioObject::RemovePropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData) { OSStatus theError = AudioObjectRemovePropertyListener(mObjectID, &inAddress, inListenerProc, inClientData); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::RemovePropertyListener: got an error removing a property listener"); } ================================================ FILE: BGMApp/PublicUtility/CAHALAudioObject.h ================================================ /* File: CAHALAudioObject.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAHALAudioObject_h__) #define __CAHALAudioObject_h__ //================================================================================================== // Includes //================================================================================================== // PublicUtility Includes #include "CADebugMacros.h" #include "CAException.h" // System Includes #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #include #else #include #include #endif //================================================================================================== // CAHALAudioObject //================================================================================================== class CAHALAudioObject { // Construction/Destruction public: CAHALAudioObject(AudioObjectID inObjectID); virtual ~CAHALAudioObject(); // Attributes public: AudioObjectID GetObjectID() const; void SetObjectID(AudioObjectID inObjectID); AudioClassID GetClassID() const; AudioObjectID GetOwnerObjectID() const; CFStringRef CopyOwningPlugInBundleID() const; CFStringRef CopyName() const; CFStringRef CopyManufacturer() const; CFStringRef CopyNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const; CFStringRef CopyCategoryNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const; CFStringRef CopyNumberNameForElement(AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) const; static bool ObjectExists(AudioObjectID inObjectID); // Owned Objects public: UInt32 GetNumberOwnedObjects(AudioClassID inClass) const; void GetAllOwnedObjects(AudioClassID inClass, UInt32& ioNumberObjects, AudioObjectID* ioObjectIDs) const; AudioObjectID GetOwnedObjectByIndex(AudioClassID inClass, UInt32 inIndex); // Property Operations public: bool HasProperty(const AudioObjectPropertyAddress& inAddress) const; bool IsPropertySettable(const AudioObjectPropertyAddress& inAddress) const; UInt32 GetPropertyDataSize(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData) const; void GetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32& ioDataSize, void* outData) const; void SetPropertyData(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* inQualifierData, UInt32 inDataSize, const void* inData); UInt32 GetPropertyData_UInt32(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { UInt32 theAnswer = 0; UInt32 theDataSize = SizeOf32(UInt32); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, &theAnswer); return theAnswer; } void SetPropertyData_UInt32(const AudioObjectPropertyAddress& inAddress, UInt32 inValue, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, SizeOf32(UInt32), &inValue); } Float32 GetPropertyData_Float32(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { Float32 theAnswer = 0; UInt32 theDataSize = SizeOf32(Float32); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, &theAnswer); return theAnswer; } void SetPropertyData_Float32(const AudioObjectPropertyAddress& inAddress, Float32 inValue, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, SizeOf32(Float32), &inValue); } Float64 GetPropertyData_Float64(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { Float64 theAnswer = 0; UInt32 theDataSize = SizeOf32(Float64); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, &theAnswer); return theAnswer; } void SetPropertyData_Float64(const AudioObjectPropertyAddress& inAddress, Float64 inValue, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, SizeOf32(Float64), &inValue); } CFTypeRef GetPropertyData_CFType(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { CFTypeRef theAnswer = NULL; UInt32 theDataSize = SizeOf32(CFTypeRef); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, &theAnswer); return theAnswer; } void SetPropertyData_CFType(const AudioObjectPropertyAddress& inAddress, CFTypeRef inValue, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, SizeOf32(CFTypeRef), &inValue); } CFStringRef GetPropertyData_CFString(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { CFStringRef theAnswer = NULL; UInt32 theDataSize = SizeOf32(CFStringRef); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, &theAnswer); return theAnswer; } void SetPropertyData_CFString(const AudioObjectPropertyAddress& inAddress, CFStringRef inValue, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, SizeOf32(CFStringRef), &inValue); } template void GetPropertyData_Struct(const AudioObjectPropertyAddress& inAddress, T& outStruct, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { UInt32 theDataSize = SizeOf32(T); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, &outStruct); } template void SetPropertyData_Struct(const AudioObjectPropertyAddress& inAddress, T& inStruct, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, SizeOf32(T), &inStruct); } template UInt32 GetPropertyData_ArraySize(const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { return GetPropertyDataSize(inAddress, inQualifierDataSize, inQualifierData) / SizeOf32(T); } template void GetPropertyData_Array(const AudioObjectPropertyAddress& inAddress, UInt32& ioNumberItems, T* outArray, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) const { UInt32 theDataSize = ioNumberItems * SizeOf32(T); GetPropertyData(inAddress, inQualifierDataSize, inQualifierData, theDataSize, outArray); ioNumberItems = theDataSize / SizeOf32(T); } template void SetPropertyData_Array(const AudioObjectPropertyAddress& inAddress, UInt32 inNumberItems, T* inArray, UInt32 inQualifierDataSize = 0, const void* inQualifierData = NULL) { SetPropertyData(inAddress, inQualifierDataSize, inQualifierData, inNumberItems * SizeOf32(T), inArray); } void AddPropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData); void RemovePropertyListener(const AudioObjectPropertyAddress& inAddress, AudioObjectPropertyListenerProc inListenerProc, void* inClientData); void AddPropertyListenerBlock(const AudioObjectPropertyAddress& inAddress, dispatch_queue_t inDispatchQueue, AudioObjectPropertyListenerBlock inListenerBlock); void RemovePropertyListenerBlock(const AudioObjectPropertyAddress& inAddress, dispatch_queue_t inDispatchQueue, AudioObjectPropertyListenerBlock inListenerBlock); // Implementation protected: AudioObjectID mObjectID; }; inline void CAHALAudioObject::AddPropertyListenerBlock(const AudioObjectPropertyAddress& inAddress, dispatch_queue_t inDispatchQueue, AudioObjectPropertyListenerBlock inListenerBlock) { OSStatus theError = AudioObjectAddPropertyListenerBlock(mObjectID, &inAddress, inDispatchQueue, inListenerBlock); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::AddPropertyListenerBlock: got an error adding a property listener"); } inline void CAHALAudioObject::RemovePropertyListenerBlock(const AudioObjectPropertyAddress& inAddress, dispatch_queue_t inDispatchQueue, AudioObjectPropertyListenerBlock inListenerBlock) { OSStatus theError = AudioObjectRemovePropertyListenerBlock(mObjectID, &inAddress, inDispatchQueue, inListenerBlock); ThrowIfError(theError, CAException(theError), "CAHALAudioObject::RemovePropertyListener: got an error removing a property listener"); } #endif ================================================ FILE: BGMApp/PublicUtility/CAHALAudioStream.cpp ================================================ /* File: CAHALAudioStream.cpp Abstract: CAHALAudioStream.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //================================================================================================== // Includes //================================================================================================== // Self Include #include "CAHALAudioStream.h" // PublicUtility Includes #include "CAAutoDisposer.h" #include "CADebugMacros.h" #include "CAException.h" #include "CAPropertyAddress.h" //================================================================================================== // CAHALAudioStream //================================================================================================== CAHALAudioStream::CAHALAudioStream(AudioObjectID inAudioStream) : CAHALAudioObject(inAudioStream) { } CAHALAudioStream::~CAHALAudioStream() { } UInt32 CAHALAudioStream::GetDirection() const { CAPropertyAddress theAddress(kAudioStreamPropertyDirection); return GetPropertyData_UInt32(theAddress, 0, NULL); } UInt32 CAHALAudioStream::GetTerminalType() const { CAPropertyAddress theAddress(kAudioStreamPropertyTerminalType); return GetPropertyData_UInt32(theAddress, 0, NULL); } UInt32 CAHALAudioStream::GetStartingChannel() const { CAPropertyAddress theAddress(kAudioStreamPropertyStartingChannel); return GetPropertyData_UInt32(theAddress, 0, NULL); } UInt32 CAHALAudioStream::GetLatency() const { CAPropertyAddress theAddress(kAudioStreamPropertyLatency); return GetPropertyData_UInt32(theAddress, 0, NULL); } void CAHALAudioStream::GetCurrentVirtualFormat(AudioStreamBasicDescription& outFormat) const { CAPropertyAddress theAddress(kAudioStreamPropertyVirtualFormat); UInt32 theSize = sizeof(AudioStreamBasicDescription); GetPropertyData(theAddress, 0, NULL, theSize, &outFormat); } void CAHALAudioStream::SetCurrentVirtualFormat(const AudioStreamBasicDescription& inFormat) { CAPropertyAddress theAddress(kAudioStreamPropertyVirtualFormat); SetPropertyData(theAddress, 0, NULL, sizeof(AudioStreamBasicDescription), &inFormat); } UInt32 CAHALAudioStream::GetNumberAvailableVirtualFormats() const { CAPropertyAddress theAddress(kAudioStreamPropertyAvailableVirtualFormats); UInt32 theAnswer = GetPropertyDataSize(theAddress, 0, NULL); theAnswer /= SizeOf32(AudioStreamRangedDescription); return theAnswer; } void CAHALAudioStream::GetAvailableVirtualFormats(UInt32& ioNumberFormats, AudioStreamRangedDescription* outFormats) const { CAPropertyAddress theAddress(kAudioStreamPropertyAvailableVirtualFormats); UInt32 theSize = ioNumberFormats * SizeOf32(AudioStreamRangedDescription); GetPropertyData(theAddress, 0, NULL, theSize, outFormats); ioNumberFormats = theSize / SizeOf32(AudioStreamRangedDescription); } void CAHALAudioStream::GetAvailableVirtualFormatByIndex(UInt32 inIndex, AudioStreamRangedDescription& outFormat) const { UInt32 theNumberFormats = GetNumberAvailableVirtualFormats(); if((theNumberFormats > 0) && (inIndex < theNumberFormats)) { CAAutoArrayDelete theFormats(theNumberFormats); GetAvailableVirtualFormats(theNumberFormats, theFormats); if((theNumberFormats > 0) && (inIndex < theNumberFormats)) { outFormat = theFormats[inIndex]; } } } void CAHALAudioStream::GetCurrentPhysicalFormat(AudioStreamBasicDescription& outFormat) const { CAPropertyAddress theAddress(kAudioStreamPropertyPhysicalFormat); UInt32 theSize = sizeof(AudioStreamBasicDescription); GetPropertyData(theAddress, 0, NULL, theSize, &outFormat); } void CAHALAudioStream::SetCurrentPhysicalFormat(const AudioStreamBasicDescription& inFormat) { CAPropertyAddress theAddress(kAudioStreamPropertyPhysicalFormat); SetPropertyData(theAddress, 0, NULL, sizeof(AudioStreamBasicDescription), &inFormat); } UInt32 CAHALAudioStream::GetNumberAvailablePhysicalFormats() const { CAPropertyAddress theAddress(kAudioStreamPropertyAvailablePhysicalFormats); UInt32 theAnswer = GetPropertyDataSize(theAddress, 0, NULL); theAnswer /= SizeOf32(AudioStreamRangedDescription); return theAnswer; } void CAHALAudioStream::GetAvailablePhysicalFormats(UInt32& ioNumberFormats, AudioStreamRangedDescription* outFormats) const { CAPropertyAddress theAddress(kAudioStreamPropertyAvailablePhysicalFormats); UInt32 theSize = ioNumberFormats * SizeOf32(AudioStreamRangedDescription); GetPropertyData(theAddress, 0, NULL, theSize, outFormats); ioNumberFormats = theSize / SizeOf32(AudioStreamRangedDescription); } void CAHALAudioStream::GetAvailablePhysicalFormatByIndex(UInt32 inIndex, AudioStreamRangedDescription& outFormat) const { UInt32 theNumberFormats = GetNumberAvailablePhysicalFormats(); if((theNumberFormats > 0) && (inIndex < theNumberFormats)) { CAAutoArrayDelete theFormats(theNumberFormats); GetAvailablePhysicalFormats(theNumberFormats, theFormats); if((theNumberFormats > 0) && (inIndex < theNumberFormats)) { outFormat = theFormats[inIndex]; } } } ================================================ FILE: BGMApp/PublicUtility/CAHALAudioStream.h ================================================ /* File: CAHALAudioStream.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAHALAudioStream_h__) #define __CAHALAudioStream_h__ //================================================================================================== // Includes //================================================================================================== // Super Class Includes #include "CAHALAudioObject.h" //================================================================================================== // CAHALAudioStream //================================================================================================== class CAHALAudioStream : public CAHALAudioObject { // Construction/Destruction public: CAHALAudioStream(AudioObjectID inAudioStream); virtual ~CAHALAudioStream(); // Attributes public: UInt32 GetDirection() const; UInt32 GetTerminalType() const; UInt32 GetStartingChannel() const; UInt32 GetLatency() const; // Format Info public: void GetCurrentVirtualFormat(AudioStreamBasicDescription& outFormat) const; void SetCurrentVirtualFormat(const AudioStreamBasicDescription& inFormat); UInt32 GetNumberAvailableVirtualFormats() const; void GetAvailableVirtualFormats(UInt32& ioNumberFormats, AudioStreamRangedDescription* outFormats) const; void GetAvailableVirtualFormatByIndex(UInt32 inIndex, AudioStreamRangedDescription& outFormat) const; void GetCurrentPhysicalFormat(AudioStreamBasicDescription& outFormat) const; void SetCurrentPhysicalFormat(const AudioStreamBasicDescription& inFormat); UInt32 GetNumberAvailablePhysicalFormats() const; void GetAvailablePhysicalFormats(UInt32& ioNumberFormats, AudioStreamRangedDescription* outFormats) const; void GetAvailablePhysicalFormatByIndex(UInt32 inIndex, AudioStreamRangedDescription& outFormat) const; }; #endif ================================================ FILE: BGMApp/PublicUtility/CAHALAudioSystemObject.cpp ================================================ /* File: CAHALAudioSystemObject.cpp Abstract: CAHALAudioSystemObject.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //================================================================================================== // Includes //================================================================================================== // Self Include #include "CAHALAudioSystemObject.h" // PublicUtility Includes #include "CAAutoDisposer.h" #include "CACFString.h" #include "CAHALAudioDevice.h" #include "CAPropertyAddress.h" //================================================================================================== // CAHALAudioSystemObject //================================================================================================== CAHALAudioSystemObject::CAHALAudioSystemObject() : CAHALAudioObject(kAudioObjectSystemObject) { } CAHALAudioSystemObject::~CAHALAudioSystemObject() { } UInt32 CAHALAudioSystemObject::GetNumberAudioDevices() const { CAPropertyAddress theAddress(kAudioHardwarePropertyDevices); UInt32 theAnswer = GetPropertyDataSize(theAddress, 0, NULL); theAnswer /= SizeOf32(AudioObjectID); return theAnswer; } void CAHALAudioSystemObject::GetAudioDevices(UInt32& ioNumberAudioDevices, AudioObjectID* outAudioDevices) const { CAPropertyAddress theAddress(kAudioHardwarePropertyDevices); UInt32 theSize = ioNumberAudioDevices * SizeOf32(AudioObjectID); GetPropertyData(theAddress, 0, NULL, theSize, outAudioDevices); ioNumberAudioDevices = theSize / SizeOf32(AudioObjectID); } AudioObjectID CAHALAudioSystemObject::GetAudioDeviceAtIndex(UInt32 inIndex) const { AudioObjectID theAnswer = kAudioObjectUnknown; UInt32 theNumberDevices = GetNumberAudioDevices(); if((theNumberDevices > 0) && (inIndex < theNumberDevices)) { CAAutoArrayDelete theDeviceList(theNumberDevices); GetAudioDevices(theNumberDevices, theDeviceList); if((theNumberDevices > 0) && (inIndex < theNumberDevices)) { theAnswer = theDeviceList[inIndex]; } } return theAnswer; } AudioObjectID CAHALAudioSystemObject::GetAudioDeviceForUID(CFStringRef inUID) const { AudioObjectID theAnswer = kAudioObjectUnknown; AudioValueTranslation theValue = { &inUID, sizeof(CFStringRef), &theAnswer, sizeof(AudioObjectID) }; CAPropertyAddress theAddress(kAudioHardwarePropertyDeviceForUID); UInt32 theSize = sizeof(AudioValueTranslation); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theAnswer; } void CAHALAudioSystemObject::LogBasicDeviceInfo() { UInt32 theNumberDevices = GetNumberAudioDevices(); CAAutoArrayDelete theDeviceList(theNumberDevices); GetAudioDevices(theNumberDevices, theDeviceList); DebugMessageN1("CAHALAudioSystemObject::LogBasicDeviceInfo: %d devices", (int)theNumberDevices); for(UInt32 theDeviceIndex = 0; theDeviceIndex < theNumberDevices; ++theDeviceIndex) { char theCString[256]; UInt32 theCStringSize = sizeof(theCString); DebugMessageN1("CAHALAudioSystemObject::LogBasicDeviceInfo: Device %d", (int)theDeviceIndex); CAHALAudioDevice theDevice(theDeviceList[theDeviceIndex]); DebugMessageN1("CAHALAudioSystemObject::LogBasicDeviceInfo: Object ID: %d", (int)theDeviceList[theDeviceIndex]); CACFString theDeviceName(theDevice.CopyName()); theCStringSize = sizeof(theCString); theDeviceName.GetCString(theCString, theCStringSize); DebugMessageN1("CAHALAudioSystemObject::LogBasicDeviceInfo: Name: %s", theCString); CACFString theDeviceUID(theDevice.CopyDeviceUID()); theCStringSize = sizeof(theCString); theDeviceUID.GetCString(theCString, theCStringSize); DebugMessageN1("CAHALAudioSystemObject::LogBasicDeviceInfo: UID: %s", theCString); } } static inline AudioObjectPropertySelector CAHALAudioSystemObject_CalculateDefaultDeviceProperySelector(bool inIsInput, bool inIsSystem) { AudioObjectPropertySelector theAnswer = kAudioHardwarePropertyDefaultOutputDevice; if(inIsInput) { theAnswer = kAudioHardwarePropertyDefaultInputDevice; } else if(inIsSystem) { theAnswer = kAudioHardwarePropertyDefaultSystemOutputDevice; } return theAnswer; } AudioObjectID CAHALAudioSystemObject::GetDefaultAudioDevice(bool inIsInput, bool inIsSystem) const { AudioObjectID theAnswer = kAudioObjectUnknown; CAPropertyAddress theAddress(CAHALAudioSystemObject_CalculateDefaultDeviceProperySelector(inIsInput, inIsSystem)); UInt32 theSize = sizeof(AudioObjectID); GetPropertyData(theAddress, 0, NULL, theSize, &theAnswer); return theAnswer; } void CAHALAudioSystemObject::SetDefaultAudioDevice(bool inIsInput, bool inIsSystem, AudioObjectID inNewDefaultDevice) { CAPropertyAddress theAddress(CAHALAudioSystemObject_CalculateDefaultDeviceProperySelector(inIsInput, inIsSystem)); UInt32 theSize = sizeof(AudioObjectID); SetPropertyData(theAddress, 0, NULL, theSize, &inNewDefaultDevice); } AudioObjectID CAHALAudioSystemObject::GetAudioPlugInForBundleID(CFStringRef inUID) const { AudioObjectID theAnswer = kAudioObjectUnknown; AudioValueTranslation theValue = { &inUID, sizeof(CFStringRef), &theAnswer, sizeof(AudioObjectID) }; CAPropertyAddress theAddress(kAudioHardwarePropertyPlugInForBundleID); UInt32 theSize = sizeof(AudioValueTranslation); GetPropertyData(theAddress, 0, NULL, theSize, &theValue); return theAnswer; } ================================================ FILE: BGMApp/PublicUtility/CAHALAudioSystemObject.h ================================================ /* File: CAHALAudioSystemObject.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAHALAudioSystemObject_h__) #define __CAHALAudioSystemObject_h__ //================================================================================================== // Includes //================================================================================================== // Super Class Includes #include "CAHALAudioObject.h" //================================================================================================== // CAHALAudioSystemObject //================================================================================================== class CAHALAudioSystemObject : public CAHALAudioObject { // Construction/Destruction public: CAHALAudioSystemObject(); virtual ~CAHALAudioSystemObject(); // Audio Device List Management public: UInt32 GetNumberAudioDevices() const; void GetAudioDevices(UInt32& ioNumberAudioDevices, AudioObjectID* outAudioDevices) const; AudioObjectID GetAudioDeviceAtIndex(UInt32 inIndex) const; AudioObjectID GetAudioDeviceForUID(CFStringRef inUID) const; void LogBasicDeviceInfo(); // Default Device Management public: AudioObjectID GetDefaultAudioDevice(bool inIsInput, bool inIsSystem) const; void SetDefaultAudioDevice(bool inIsInput, bool inIsSystem, AudioObjectID inNewDefaultDevice); // PlugIns public: AudioObjectID GetAudioPlugInForBundleID(CFStringRef inBundleID) const; }; #endif ================================================ FILE: BGMApp/PublicUtility/CAHostTimeBase.cpp ================================================ /* File: CAHostTimeBase.cpp Abstract: CAHostTimeBase.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= #include "CAHostTimeBase.h" Float64 CAHostTimeBase::sFrequency = 0; Float64 CAHostTimeBase::sInverseFrequency = 0; UInt32 CAHostTimeBase::sMinDelta = 0; UInt32 CAHostTimeBase::sToNanosNumerator = 0; UInt32 CAHostTimeBase::sToNanosDenominator = 0; pthread_once_t CAHostTimeBase::sIsInited = PTHREAD_ONCE_INIT; #if Track_Host_TimeBase UInt64 CAHostTimeBase::sLastTime = 0; #endif //============================================================================= // CAHostTimeBase // // This class provides platform independent access to the host's time base. //============================================================================= void CAHostTimeBase::Initialize() { // get the info about Absolute time #if TARGET_OS_MAC struct mach_timebase_info theTimeBaseInfo; mach_timebase_info(&theTimeBaseInfo); sMinDelta = 1; sToNanosNumerator = theTimeBaseInfo.numer; sToNanosDenominator = theTimeBaseInfo.denom; // the frequency of that clock is: (sToNanosDenominator / sToNanosNumerator) * 10^9 sFrequency = static_cast(sToNanosDenominator) / static_cast(sToNanosNumerator); sFrequency *= 1000000000.0; #elif TARGET_OS_WIN32 LARGE_INTEGER theFrequency; QueryPerformanceFrequency(&theFrequency); sMinDelta = 1; sToNanosNumerator = 1000000000ULL; sToNanosDenominator = *((UInt64*)&theFrequency); sFrequency = static_cast(*((UInt64*)&theFrequency)); #endif sInverseFrequency = 1.0 / sFrequency; #if Log_Host_Time_Base_Parameters DebugPrintf("Host Time Base Parameters"); DebugPrintf(" Minimum Delta: %lu", (unsigned long)sMinDelta); DebugPrintf(" Frequency: %f", sFrequency); DebugPrintf(" To Nanos Numerator: %lu", (unsigned long)sToNanosNumerator); DebugPrintf(" To Nanos Denominator: %lu", (unsigned long)sToNanosDenominator); #endif } ================================================ FILE: BGMApp/PublicUtility/CAHostTimeBase.h ================================================ /* File: CAHostTimeBase.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #if !defined(__CAHostTimeBase_h__) #define __CAHostTimeBase_h__ //============================================================================= // Includes //============================================================================= #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include #endif #if TARGET_OS_MAC #include #include #elif TARGET_OS_WIN32 #include #include "WinPThreadDefs.h" #else #error Unsupported operating system #endif #include "CADebugPrintf.h" //============================================================================= // CAHostTimeBase // // This class provides platform independent access to the host's time base. //============================================================================= #if CoreAudio_Debug // #define Log_Host_Time_Base_Parameters 1 // #define Track_Host_TimeBase 1 #endif class CAHostTimeBase { public: static UInt64 ConvertToNanos(UInt64 inHostTime); static UInt64 ConvertFromNanos(UInt64 inNanos); static UInt64 GetTheCurrentTime(); #if TARGET_OS_MAC static UInt64 GetCurrentTime() { return GetTheCurrentTime(); } #endif static UInt64 GetCurrentTimeInNanos(); static Float64 GetFrequency() { pthread_once(&sIsInited, Initialize); return sFrequency; } static Float64 GetInverseFrequency() { pthread_once(&sIsInited, Initialize); return sInverseFrequency; } static UInt32 GetMinimumDelta() { pthread_once(&sIsInited, Initialize); return sMinDelta; } static UInt64 AbsoluteHostDeltaToNanos(UInt64 inStartTime, UInt64 inEndTime); static SInt64 HostDeltaToNanos(UInt64 inStartTime, UInt64 inEndTime); static UInt64 MultiplyByRatio(UInt64 inMuliplicand, UInt32 inNumerator, UInt32 inDenominator); private: static void Initialize(); static pthread_once_t sIsInited; static Float64 sFrequency; static Float64 sInverseFrequency; static UInt32 sMinDelta; static UInt32 sToNanosNumerator; static UInt32 sToNanosDenominator; #if Track_Host_TimeBase static UInt64 sLastTime; #endif }; inline UInt64 CAHostTimeBase::GetTheCurrentTime() { UInt64 theTime = 0; #if TARGET_OS_MAC theTime = mach_absolute_time(); #elif TARGET_OS_WIN32 LARGE_INTEGER theValue; QueryPerformanceCounter(&theValue); theTime = *((UInt64*)&theValue); #endif #if Track_Host_TimeBase if(sLastTime != 0) { if(theTime <= sLastTime) { DebugPrintf("CAHostTimeBase::GetTheCurrentTime: the current time is earlier than the last time, now: %qd, then: %qd", theTime, sLastTime); } sLastTime = theTime; } else { sLastTime = theTime; } #endif return theTime; } inline UInt64 CAHostTimeBase::ConvertToNanos(UInt64 inHostTime) { pthread_once(&sIsInited, Initialize); UInt64 theAnswer = MultiplyByRatio(inHostTime, sToNanosNumerator, sToNanosDenominator); #if CoreAudio_Debug if(((sToNanosNumerator > sToNanosDenominator) && (theAnswer < inHostTime)) || ((sToNanosDenominator > sToNanosNumerator) && (theAnswer > inHostTime))) { DebugPrintf("CAHostTimeBase::ConvertToNanos: The conversion wrapped"); } #endif return theAnswer; } inline UInt64 CAHostTimeBase::ConvertFromNanos(UInt64 inNanos) { pthread_once(&sIsInited, Initialize); UInt64 theAnswer = MultiplyByRatio(inNanos, sToNanosDenominator, sToNanosNumerator); #if CoreAudio_Debug if(((sToNanosDenominator > sToNanosNumerator) && (theAnswer < inNanos)) || ((sToNanosNumerator > sToNanosDenominator) && (theAnswer > inNanos))) { DebugPrintf("CAHostTimeBase::ConvertFromNanos: The conversion wrapped"); } #endif return theAnswer; } inline UInt64 CAHostTimeBase::GetCurrentTimeInNanos() { return ConvertToNanos(GetTheCurrentTime()); } inline UInt64 CAHostTimeBase::AbsoluteHostDeltaToNanos(UInt64 inStartTime, UInt64 inEndTime) { UInt64 theAnswer; if(inStartTime <= inEndTime) { theAnswer = inEndTime - inStartTime; } else { theAnswer = inStartTime - inEndTime; } return ConvertToNanos(theAnswer); } inline SInt64 CAHostTimeBase::HostDeltaToNanos(UInt64 inStartTime, UInt64 inEndTime) { SInt64 theAnswer; SInt64 theSign = 1; if(inStartTime <= inEndTime) { theAnswer = static_cast(inEndTime - inStartTime); } else { theAnswer = static_cast(inStartTime - inEndTime); theSign = -1; } return theSign * static_cast(ConvertToNanos(static_cast(theAnswer))); } inline UInt64 CAHostTimeBase::MultiplyByRatio(UInt64 inMuliplicand, UInt32 inNumerator, UInt32 inDenominator) { #if TARGET_OS_MAC && TARGET_RT_64_BIT __uint128_t theAnswer = inMuliplicand; #else long double theAnswer = inMuliplicand; #endif if(inNumerator != inDenominator) { theAnswer *= inNumerator; theAnswer /= inDenominator; } return static_cast(theAnswer); } #endif ================================================ FILE: BGMApp/PublicUtility/CAMutex.cpp ================================================ /* File: CAMutex.cpp Abstract: CAMutex.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //================================================================================================== // Includes //================================================================================================== // Self Include #include "CAMutex.h" #if TARGET_OS_MAC #include #endif // PublicUtility Includes #include "CADebugMacros.h" #include "CAException.h" #include "CAHostTimeBase.h" //================================================================================================== // Logging //================================================================================================== #if CoreAudio_Debug // #define Log_Ownership 1 // #define Log_Errors 1 // #define Log_LongLatencies 1 // #define LongLatencyThreshholdNS 1000000ULL // nanoseconds #endif //================================================================================================== // CAMutex //================================================================================================== CAMutex::CAMutex(const char* inName) : mName(inName), mOwner(0) { #if TARGET_OS_MAC OSStatus theError = pthread_mutex_init(&mMutex, NULL); ThrowIf(theError != 0, CAException(theError), "CAMutex::CAMutex: Could not init the mutex"); #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::CAMutex: creating %s, owner: %p\n", pthread_self(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), mName, mOwner); #endif #elif TARGET_OS_WIN32 mMutex = CreateMutex(NULL, false, NULL); ThrowIfNULL(mMutex, CAException(GetLastError()), "CAMutex::CAMutex: could not create the mutex."); #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::CAMutex: creating %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), mName, mOwner); #endif #endif } CAMutex::~CAMutex() { #if TARGET_OS_MAC #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::~CAMutex: destroying %s, owner: %p\n", pthread_self(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), mName, mOwner); #endif pthread_mutex_destroy(&mMutex); #elif TARGET_OS_WIN32 #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::~CAMutex: destroying %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), mName, mOwner); #endif if(mMutex != NULL) { CloseHandle(mMutex); } #endif } bool CAMutex::Lock() { bool theAnswer = false; #if TARGET_OS_MAC pthread_t theCurrentThread = pthread_self(); if(!pthread_equal(theCurrentThread, mOwner)) { #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Lock: thread %p is locking %s, owner: %p\n", theCurrentThread, ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), theCurrentThread, mName, mOwner); #endif #if Log_LongLatencies UInt64 lockTryTime = CAHostTimeBase::GetCurrentTimeInNanos(); #endif OSStatus theError = pthread_mutex_lock(&mMutex); ThrowIf(theError != 0, CAException(theError), "CAMutex::Lock: Could not lock the mutex"); mOwner = theCurrentThread; theAnswer = true; #if Log_LongLatencies UInt64 lockAcquireTime = CAHostTimeBase::GetCurrentTimeInNanos(); if (lockAcquireTime - lockTryTime >= LongLatencyThresholdNS) DebugPrintfRtn(DebugPrintfFileComma "Thread %p took %.6fs to acquire the lock %s\n", theCurrentThread, (lockAcquireTime - lockTryTime) * 1.0e-9 /* nanos to seconds */, mName); #endif #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Lock: thread %p has locked %s, owner: %p\n", pthread_self(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), pthread_self(), mName, mOwner); #endif } #elif TARGET_OS_WIN32 if(mOwner != GetCurrentThreadId()) { #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Lock: thread %lu is locking %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif OSStatus theError = WaitForSingleObject(mMutex, INFINITE); ThrowIfError(theError, CAException(theError), "CAMutex::Lock: could not lock the mutex"); mOwner = GetCurrentThreadId(); theAnswer = true; #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Lock: thread %lu has locked %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif } #endif return theAnswer; } void CAMutex::Unlock() { #if TARGET_OS_MAC if(pthread_equal(pthread_self(), mOwner)) { #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Unlock: thread %p is unlocking %s, owner: %p\n", pthread_self(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), pthread_self(), mName, mOwner); #endif mOwner = 0; OSStatus theError = pthread_mutex_unlock(&mMutex); ThrowIf(theError != 0, CAException(theError), "CAMutex::Unlock: Could not unlock the mutex"); #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Unlock: thread %p has unlocked %s, owner: %p\n", pthread_self(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), pthread_self(), mName, mOwner); #endif } else { DebugMessage("CAMutex::Unlock: A thread is attempting to unlock a Mutex it doesn't own"); } #elif TARGET_OS_WIN32 if(mOwner == GetCurrentThreadId()) { #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Unlock: thread %lu is unlocking %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif mOwner = 0; bool wasReleased = ReleaseMutex(mMutex); ThrowIf(!wasReleased, CAException(GetLastError()), "CAMutex::Unlock: Could not unlock the mutex"); #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Unlock: thread %lu has unlocked %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif } else { DebugMessage("CAMutex::Unlock: A thread is attempting to unlock a Mutex it doesn't own"); } #endif } bool CAMutex::Try(bool& outWasLocked) { bool theAnswer = false; outWasLocked = false; #if TARGET_OS_MAC pthread_t theCurrentThread = pthread_self(); if(!pthread_equal(theCurrentThread, mOwner)) { // this means the current thread doesn't already own the lock #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Try: thread %p is try-locking %s, owner: %p\n", theCurrentThread, ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), theCurrentThread, mName, mOwner); #endif // go ahead and call trylock to see if we can lock it. int theError = pthread_mutex_trylock(&mMutex); if(theError == 0) { // return value of 0 means we successfully locked the lock mOwner = theCurrentThread; theAnswer = true; outWasLocked = true; #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Try: thread %p has locked %s, owner: %p\n", theCurrentThread, ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), theCurrentThread, mName, mOwner); #endif } else if(theError == EBUSY) { // return value of EBUSY means that the lock was already locked by another thread theAnswer = false; outWasLocked = false; #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%p %.4f: CAMutex::Try: thread %p failed to lock %s, owner: %p\n", theCurrentThread, ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), theCurrentThread, mName, mOwner); #endif } else { // any other return value means something really bad happenned ThrowIfError(theError, CAException(theError), "CAMutex::Try: call to pthread_mutex_trylock failed"); } } else { // this means the current thread already owns the lock theAnswer = true; outWasLocked = false; } #elif TARGET_OS_WIN32 if(mOwner != GetCurrentThreadId()) { // this means the current thread doesn't own the lock #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Try: thread %lu is try-locking %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif // try to acquire the mutex OSStatus theError = WaitForSingleObject(mMutex, 0); if(theError == WAIT_OBJECT_0) { // this means we successfully locked the lock mOwner = GetCurrentThreadId(); theAnswer = true; outWasLocked = true; #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Try: thread %lu has locked %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif } else if(theError == WAIT_TIMEOUT) { // this means that the lock was already locked by another thread theAnswer = false; outWasLocked = false; #if Log_Ownership DebugPrintfRtn(DebugPrintfFileComma "%lu %.4f: CAMutex::Try: thread %lu failed to lock %s, owner: %lu\n", GetCurrentThreadId(), ((Float64)(CAHostTimeBase::GetCurrentTimeInNanos()) / 1000000.0), GetCurrentThreadId(), mName, mOwner); #endif } else { // any other return value means something really bad happenned ThrowIfError(theError, CAException(GetLastError()), "CAMutex::Try: call to lock the mutex failed"); } } else { // this means the current thread already owns the lock theAnswer = true; outWasLocked = false; } #endif return theAnswer; } bool CAMutex::IsFree() const { return mOwner == 0; } bool CAMutex::IsOwnedByCurrentThread() const { bool theAnswer = true; #if TARGET_OS_MAC theAnswer = pthread_equal(pthread_self(), mOwner); #elif TARGET_OS_WIN32 theAnswer = (mOwner == GetCurrentThreadId()); #endif return theAnswer; } CAMutex::Unlocker::Unlocker(CAMutex& inMutex) : mMutex(inMutex), mNeedsLock(false) { Assert(mMutex.IsOwnedByCurrentThread(), "Major problem: Unlocker attempted to unlock a mutex not owned by the current thread!"); mMutex.Unlock(); mNeedsLock = true; } CAMutex::Unlocker::~Unlocker() { if(mNeedsLock) { mMutex.Lock(); } } ================================================ FILE: BGMApp/PublicUtility/CAMutex.h ================================================ // 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 . // // CAMutex.h // PublicUtility // // Copyright (C) 2014 Apple Inc. All Rights Reserved. // Copyright © 2020 Kyle Neideck // // Original license header follows. // /* File: CAMutex.h Abstract: Part of CoreAudio Utility Classes Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ #ifndef __CAMutex_h__ #define __CAMutex_h__ //================================================================================================== // Includes //================================================================================================== #include "BGMThreadSafetyAnalysis.h" // System Includes #if !defined(__COREAUDIO_USE_FLAT_INCLUDES__) #include #else #include #endif #if TARGET_OS_MAC #include #elif TARGET_OS_WIN32 #include #else #error Unsupported operating system #endif //================================================================================================== // A recursive mutex. //================================================================================================== class CAPABILITY("mutex") CAMutex { // Construction/Destruction public: CAMutex(const char* inName); virtual ~CAMutex(); // Actions public: virtual bool Lock() ACQUIRE(); virtual void Unlock() RELEASE(); virtual bool Try(bool& outWasLocked) TRY_ACQUIRE(true); // returns true if lock is free, false if not virtual bool IsFree() const; virtual bool IsOwnedByCurrentThread() const; // Implementation protected: const char* mName; #if TARGET_OS_MAC pthread_t mOwner; pthread_mutex_t mMutex; #elif TARGET_OS_WIN32 UInt32 mOwner; HANDLE mMutex; #endif // Helper class to manage taking and releasing recursively public: class SCOPED_CAPABILITY Locker { // Construction/Destruction public: Locker(CAMutex& inMutex) ACQUIRE(inMutex) : mMutex(&inMutex), mNeedsRelease(false) { mNeedsRelease = mMutex->Lock(); } Locker(CAMutex* inMutex) ACQUIRE(inMutex) : mMutex(inMutex), mNeedsRelease(false) { mNeedsRelease = (mMutex != NULL && mMutex->Lock()); } // in this case the mutex can be null ~Locker() RELEASE() { if(mNeedsRelease) { mMutex->Unlock(); } } private: Locker(const Locker&); Locker& operator=(const Locker&); // Implementation private: CAMutex* mMutex; bool mNeedsRelease; }; // Clang's static analysis doesn't work for unlocker classes. See // . // Unlocker class Unlocker { public: Unlocker(CAMutex& inMutex); ~Unlocker(); private: CAMutex& mMutex; bool mNeedsLock; // Hidden definitions of copy ctor, assignment operator Unlocker(const Unlocker& copy); // Not implemented Unlocker& operator=(const Unlocker& copy); // Not implemented }; // you can use this with Try - if you take the lock in try, pass in the outWasLocked var class SCOPED_CAPABILITY Tryer { // Construction/Destruction public: Tryer (CAMutex &mutex) TRY_ACQUIRE(true, mutex) : mMutex(mutex), mNeedsRelease(false), mHasLock(false) { mHasLock = mMutex.Try (mNeedsRelease); } ~Tryer () RELEASE() { if (mNeedsRelease) mMutex.Unlock(); } bool HasLock () const { return mHasLock; } private: Tryer(const Tryer&); Tryer& operator=(const Tryer&); // Implementation private: CAMutex & mMutex; bool mNeedsRelease; bool mHasLock; }; }; #endif // __CAMutex_h__ ================================================ FILE: BGMApp/PublicUtility/CAPThread.cpp ================================================ /* File: CAPThread.cpp Abstract: CAPThread.h Version: 1.1 Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple Inc. ("Apple") in consideration of your agreement to the following terms, and your use, installation, modification or redistribution of this Apple software constitutes acceptance of these terms. If you do not agree with these terms, please do not use, install, modify or redistribute this Apple software. In consideration of your agreement to abide by the following terms, and subject to these terms, Apple grants you a personal, non-exclusive license, under Apple's copyrights in this original Apple software (the "Apple Software"), to use, reproduce, modify and redistribute the Apple Software, with or without modifications, in source and/or binary forms; provided that if you redistribute the Apple Software in its entirety and without modifications, you must retain this notice and the following text and disclaimers in all such redistributions of the Apple Software. Neither the name, trademarks, service marks or logos of Apple Inc. may be used to endorse or promote products derived from the Apple Software without specific prior written permission from Apple. Except as expressly stated in this notice, no other rights or licenses, express or implied, are granted by Apple herein, including but not limited to any patent rights that may be infringed by your derivative works or by other works in which the Apple Software may be incorporated. The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS. IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. Copyright (C) 2014 Apple Inc. All Rights Reserved. */ //============================================================================= // Includes //============================================================================= // Self Include #include "CAPThread.h" // PublicUtility Includes #include "CADebugMacros.h" #include "CAException.h" // System Includes #if TARGET_OS_MAC #include #endif // Standard Library Includes #include //================================================================================================== // CAPThread //================================================================================================== // returns the thread's priority as it was last set by the API #define CAPTHREAD_SET_PRIORITY 0 // returns the thread's priority as it was last scheduled by the Kernel #define CAPTHREAD_SCHEDULED_PRIORITY 1 //#define Log_SetPriority 1 CAPThread::CAPThread(ThreadRoutine inThreadRoutine, void* inParameter, UInt32 inPriority, bool inFixedPriority, bool inAutoDelete, const char* inThreadName) : #if TARGET_OS_MAC mPThread(0), mSpawningThreadPriority(getScheduledPriority(pthread_self(), CAPTHREAD_SET_PRIORITY)), #elif TARGET_OS_WIN32 mThreadHandle(NULL), mThreadID(0), #endif mThreadRoutine(inThreadRoutine), mThreadParameter(inParameter), mPriority(inPriority), mPeriod(0), mComputation(0), mConstraint(0), mIsPreemptible(true), mTimeConstraintSet(false), mFixedPriority(inFixedPriority), mAutoDelete(inAutoDelete) { if(inThreadName != NULL) { strlcpy(mThreadName, inThreadName, kMaxThreadNameLength); } else { memset(mThreadName, 0, kMaxThreadNameLength); } } CAPThread::CAPThread(ThreadRoutine inThreadRoutine, void* inParameter, UInt32 inPeriod, UInt32 inComputation, UInt32 inConstraint, bool inIsPreemptible, bool inAutoDelete, const char* inThreadName) : #if TARGET_OS_MAC mPThread(0), mSpawningThreadPriority(getScheduledPriority(pthread_self(), CAPTHREAD_SET_PRIORITY)), #elif TARGET_OS_WIN32 mThreadHandle(NULL), mThreadID(0), #endif mThreadRoutine(inThreadRoutine), mThreadParameter(inParameter), mPriority(kDefaultThreadPriority), mPeriod(inPeriod), mComputation(inComputation), mConstraint(inConstraint), mIsPreemptible(inIsPreemptible), mTimeConstraintSet(true), mFixedPriority(false), mAutoDelete(inAutoDelete) { if(inThreadName != NULL) { strlcpy(mThreadName, inThreadName, kMaxThreadNameLength); } else { memset(mThreadName, 0, kMaxThreadNameLength); } } CAPThread::~CAPThread() { } UInt32 CAPThread::GetScheduledPriority() { #if TARGET_OS_MAC return CAPThread::getScheduledPriority( mPThread, CAPTHREAD_SCHEDULED_PRIORITY ); #elif TARGET_OS_WIN32 UInt32 theAnswer = 0; if(mThreadHandle != NULL) { theAnswer = GetThreadPriority(mThreadHandle); } return theAnswer; #endif } UInt32 CAPThread::GetScheduledPriority(NativeThread thread) { #if TARGET_OS_MAC return getScheduledPriority( thread, CAPTHREAD_SCHEDULED_PRIORITY ); #elif TARGET_OS_WIN32 return 0; // ??? #endif } void CAPThread::SetPriority(UInt32 inPriority, bool inFixedPriority) { mPriority = inPriority; mTimeConstraintSet = false; mFixedPriority = inFixedPriority; #if TARGET_OS_MAC if(mPThread != 0) { SetPriority(mPThread, mPriority, mFixedPriority); } #elif TARGET_OS_WIN32 if(mThreadID != NULL) { SetPriority(mThreadID, mPriority, mFixedPriority); } #endif } void CAPThread::SetPriority(NativeThread inThread, UInt32 inPriority, bool inFixedPriority) { #if TARGET_OS_MAC if(inThread != 0) { kern_return_t theError = 0; // set whether or not this is a fixed priority thread if (inFixedPriority) { thread_extended_policy_data_t theFixedPolicy = { false }; theError = thread_policy_set(pthread_mach_thread_np(inThread), THREAD_EXTENDED_POLICY, (thread_policy_t)&theFixedPolicy, THREAD_EXTENDED_POLICY_COUNT); AssertNoKernelError(theError, "CAPThread::SetPriority: failed to set the fixed-priority policy"); } // set the thread's absolute priority which is relative to the priority on which thread_policy_set() is called UInt32 theCurrentThreadPriority = getScheduledPriority(pthread_self(), CAPTHREAD_SET_PRIORITY); thread_precedence_policy_data_t thePrecedencePolicy = { static_cast(inPriority - theCurrentThreadPriority) }; theError = thread_policy_set(pthread_mach_thread_np(inThread), THREAD_PRECEDENCE_POLICY, (thread_policy_t)&thePrecedencePolicy, THREAD_PRECEDENCE_POLICY_COUNT); AssertNoKernelError(theError, "CAPThread::SetPriority: failed to set the precedence policy"); #if Log_SetPriority DebugMessageN4("CAPThread::SetPriority: requsted: %lu spawning: %lu current: %lu assigned: %d", mPriority, mSpawningThreadPriority, theCurrentThreadPriority, thePrecedencePolicy.importance); #endif } #elif TARGET_OS_WIN32 if(inThread != NULL) { HANDLE hThread = OpenThread(NULL, FALSE, inThread); if(hThread != NULL) { SetThreadPriority(hThread, inPriority); CloseHandle(hThread); } } #endif } void CAPThread::SetTimeConstraints(UInt32 inPeriod, UInt32 inComputation, UInt32 inConstraint, bool inIsPreemptible) { mPeriod = inPeriod; mComputation = inComputation; mConstraint = inConstraint; mIsPreemptible = inIsPreemptible; mTimeConstraintSet = true; #if TARGET_OS_MAC if(mPThread != 0) { thread_time_constraint_policy_data_t thePolicy; thePolicy.period = mPeriod; thePolicy.computation = mComputation; thePolicy.constraint = mConstraint; thePolicy.preemptible = mIsPreemptible; AssertNoError(thread_policy_set(pthread_mach_thread_np(mPThread), THREAD_TIME_CONSTRAINT_POLICY, (thread_policy_t)&thePolicy, THREAD_TIME_CONSTRAINT_POLICY_COUNT), "CAPThread::SetTimeConstraints: thread_policy_set failed"); } #elif TARGET_OS_WIN32 if(mThreadHandle != NULL) { SetThreadPriority(mThreadHandle, THREAD_PRIORITY_TIME_CRITICAL); } #endif } void CAPThread::Start() { #if TARGET_OS_MAC Assert(mPThread == 0, "CAPThread::Start: can't start because the thread is already running"); if(mPThread == 0) { OSStatus theResult; pthread_attr_t theThreadAttributes; theResult = pthread_attr_init(&theThreadAttributes); ThrowIf(theResult != 0, CAException(theResult), "CAPThread::Start: Thread attributes could not be created."); theResult = pthread_attr_setdetachstate(&theThreadAttributes, PTHREAD_CREATE_DETACHED); ThrowIf(theResult != 0, CAException(theResult), "CAPThread::Start: A thread could not be created in the detached state."); theResult = pthread_create(&mPThread, &theThreadAttributes, (ThreadRoutine)CAPThread::Entry, this); ThrowIf(theResult != 0 || !mPThread, CAException(theResult), "CAPThread::Start: Could not create a thread."); pthread_attr_destroy(&theThreadAttributes); } #elif TARGET_OS_WIN32 Assert(mThreadID == 0, "CAPThread::Start: can't start because the thread is already running"); if(mThreadID == 0) { // clean up the existing thread handle if(mThreadHandle != NULL) { CloseHandle(mThreadHandle); mThreadHandle = NULL; } // create a new thread mThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Entry, this, 0, &mThreadID); ThrowIf(mThreadHandle == NULL, CAException(GetLastError()), "CAPThread::Start: Could not create a thread."); } #endif } #if TARGET_OS_MAC void* CAPThread::Entry(CAPThread* inCAPThread) { void* theAnswer = NULL; #if TARGET_OS_MAC inCAPThread->mPThread = pthread_self(); #elif TARGET_OS_WIN32 // do we need to do something here? #endif #if !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6) if(inCAPThread->mThreadName[0] != 0) { pthread_setname_np(inCAPThread->mThreadName); } #endif try { if(inCAPThread->mTimeConstraintSet) { inCAPThread->SetTimeConstraints(inCAPThread->mPeriod, inCAPThread->mComputation, inCAPThread->mConstraint, inCAPThread->mIsPreemptible); } else { inCAPThread->SetPriority(inCAPThread->mPriority, inCAPThread->mFixedPriority); } if(inCAPThread->mThreadRoutine != NULL) { theAnswer = inCAPThread->mThreadRoutine(inCAPThread->mThreadParameter); } } catch (...) { // what should be done here? } inCAPThread->mPThread = 0; if (inCAPThread->mAutoDelete) delete inCAPThread; return theAnswer; } UInt32 CAPThread::getScheduledPriority(pthread_t inThread, int inPriorityKind) { thread_basic_info_data_t threadInfo; policy_info_data_t thePolicyInfo; unsigned int count; if (inThread == NULL) return 0; // get basic info count = THREAD_BASIC_INFO_COUNT; thread_info (pthread_mach_thread_np (inThread), THREAD_BASIC_INFO, (thread_info_t)&threadInfo, &count); switch (threadInfo.policy) { case POLICY_TIMESHARE: count = POLICY_TIMESHARE_INFO_COUNT; thread_info(pthread_mach_thread_np (inThread), THREAD_SCHED_TIMESHARE_INFO, (thread_info_t)&(thePolicyInfo.ts), &count); if (inPriorityKind == CAPTHREAD_SCHEDULED_PRIORITY) { return static_cast(thePolicyInfo.ts.cur_priority); } return static_cast(thePolicyInfo.ts.base_priority); break; case POLICY_FIFO: count = POLICY_FIFO_INFO_COUNT; thread_info(pthread_mach_thread_np (inThread), THREAD_SCHED_FIFO_INFO, (thread_info_t)&(thePolicyInfo.fifo), &count); if ( (thePolicyInfo.fifo.depressed) && (inPriorityKind == CAPTHREAD_SCHEDULED_PRIORITY) ) { return static_cast(thePolicyInfo.fifo.depress_priority); } return static_cast(thePolicyInfo.fifo.base_priority); break; case POLICY_RR: count = POLICY_RR_INFO_COUNT; thread_info(pthread_mach_thread_np (inThread), THREAD_SCHED_RR_INFO, (thread_info_t)&(thePolicyInfo.rr), &count); if ( (thePolicyInfo.rr.depressed) && (inPriorityKind == CAPTHREAD_SCHEDULED_PRIORITY) ) { return static_cast(thePolicyInfo.rr.depress_priority); } return static_cast(thePolicyInfo.rr.base_priority); break; } return 0; } #elif TARGET_OS_WIN32 UInt32 WINAPI CAPThread::Entry(CAPThread* inCAPThread) { UInt32 theAnswer = 0; try { if(inCAPThread->mTimeConstraintSet) { inCAPThread->SetTimeConstraints(inCAPThread->mPeriod, inCAPThread->mComputation, inCAPThread->mConstraint, inCAPThread->mIsPreemptible); } else { inCAPThread->SetPriority(inCAPThread->mPriority, inCAPThread->mFixedPriority); } if(inCAPThread->mThreadRoutine != NULL) { theAnswer = reinterpret_cast(inCAPThread->mThreadRoutine(inCAPThread->mThreadParameter)); } inCAPThread->mThreadID = 0; } catch (...) { // what should be done here? } CloseHandle(inCAPThread->mThreadHandle); inCAPThread->mThreadHandle = NULL; if (inCAPThread->mAutoDelete) delete inCAPThread; return theAnswer; } extern "C" Boolean CompareAndSwap(UInt32 inOldValue, UInt32 inNewValue, UInt32* inOldValuePtr) { return InterlockedCompareExchange((volatile LONG*)inOldValuePtr, inNewValue, inOldValue) == inOldValue; } #endif void CAPThread::SetName(const char* inThreadName) { if(inThreadName != NULL) { strlcpy(mThreadName, inThreadName, kMaxThreadNameLength); } else { memset(mThreadName, 0, kMaxThreadNameLength); } } #if CoreAudio_Debug void CAPThread::DebugPriority(const char *label) { #if !TARGET_OS_WIN32 if (mTimeConstraintSet) printf("CAPThread::%s %p: pri=