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