BGMDevice: Only enable volume/mute if the output device also has them.

BGMApp now disables BGMDevice's volume and/or mute controls if the
output device selected in BGMApp doesn't have matching controls. This
prevents the controls from being presented to the user when they don't
do anything.

In BGMPlayThrough, wait much longer for our IOProcs to stop themselves
before assuming something's gone wrong. In testing, rapidly changing
between output devices with and without controls while playing audio
would occasionally cause one of the IOProcs to take too long to stop
itself.

Also adds some basic scriptability, mainly so UI tests can use
AppleScript to check BGMApp's state that would be complicated to check
otherwise. (In this case, to check which output device is selected.)

Fixes #101.
This commit is contained in:
Kyle Neideck 2017-05-30 23:15:36 +10:00
parent 4839ea8a4b
commit c617d98f9d
No known key found for this signature in database
GPG key ID: CAA8D9B8E39EC18C
43 changed files with 4981 additions and 1218 deletions

View file

@ -24,10 +24,19 @@
1C1963091BCAF677008A4DF7 /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; };
1C2336DA1BEAB6E7004C1C4E /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; };
1C2336DF1BEAE10C004C1C4E /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; };
1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */; };
1C2FC3141EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; };
1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */; };
1C2FC31B1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; };
1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */; };
1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; };
1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; };
1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */; };
1C3DB4891BE0885A00EC8160 /* BGMAppVolumes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */; };
1C4699471BD5C0E400F78043 /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; };
1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; };
1CB8B33D1BBA75EF000E2DD1 /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* AppDelegate.mm */; };
1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
1CB8B33D1BBA75EF000E2DD1 /* BGMAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */; };
1CB8B33F1BBA75EF000E2DD1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33E1BBA75EF000E2DD1 /* main.m */; };
1CC1DF811BE5068A00FB8FE4 /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; };
1CC1DF821BE5068A00FB8FE4 /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; };
@ -38,9 +47,50 @@
1CCC4F4E1E581C40008053E4 /* Mock_CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F4C1E581C40008053E4 /* Mock_CAHALAudioObject.cpp */; };
1CCC4F621E584100008053E4 /* BGMAppUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CCC4F611E584100008053E4 /* BGMAppUITests.m */; };
1CD1FD301BDDEAF2004F7E1B /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
1CD989341ECFFC9E0014BBBF /* BGM_Utils.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27FB8C2E1DE468320084DB9D /* BGM_Utils.cpp */; };
1CD989351ECFFC9E0014BBBF /* CACFArray.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */; };
1CD989361ECFFC9E0014BBBF /* CACFDictionary.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF7F1BE5068A00FB8FE4 /* CACFDictionary.cpp */; };
1CD989371ECFFC9E0014BBBF /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; };
1CD989381ECFFC9E0014BBBF /* CACFString.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FF1BCAC0F6008A4DF7 /* CACFString.cpp */; };
1CD989391ECFFC9E0014BBBF /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF8F1BE5891300FB8FE4 /* CADebugger.cpp */; };
1CD9893A1ECFFC9E0014BBBF /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F71BCAC061008A4DF7 /* CADebugMacros.cpp */; };
1CD9893B1ECFFC9E0014BBBF /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962FB1BCAC0C3008A4DF7 /* CADebugPrintf.cpp */; };
1CD9893C1ECFFC9E0014BBBF /* CAHALAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EB1BCABFC5008A4DF7 /* CAHALAudioDevice.cpp */; };
1CD9893D1ECFFC9E0014BBBF /* CAHALAudioObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962ED1BCABFC5008A4DF7 /* CAHALAudioObject.cpp */; };
1CD9893E1ECFFC9E0014BBBF /* CAHALAudioStream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962EF1BCABFC5008A4DF7 /* CAHALAudioStream.cpp */; };
1CD9893F1ECFFC9E0014BBBF /* CAHALAudioSystemObject.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962F11BCABFC5008A4DF7 /* CAHALAudioSystemObject.cpp */; };
1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */; };
1CD989411ECFFCD10014BBBF /* BGMAppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */; };
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */; };
1CD989431ECFFCFC0014BBBF /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; };
1CD989441ECFFCFC0014BBBF /* BGMAutoPauseMenuItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */; };
1CD989451ECFFCFC0014BBBF /* BGMAutoPauseMusic.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C1465B71BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm */; };
1CD989461ECFFCFC0014BBBF /* BGMMusicPlayers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9E81D852B350089613B /* BGMMusicPlayers.mm */; };
1CD989471ECFFCFC0014BBBF /* BGMScriptingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9EA1D852B350089613B /* BGMScriptingBridge.m */; };
1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336D91BEAB6E7004C1C4E /* BGMMusicPlayer.m */; };
1CD989491ECFFCFC0014BBBF /* BGMDecibel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27F7D48F1D2483B100821C4B /* BGMDecibel.m */; };
1CD9894A1ECFFCFC0014BBBF /* BGMiTunes.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C4699461BD5C0E400F78043 /* BGMiTunes.m */; };
1CD9894B1ECFFCFC0014BBBF /* BGMSpotify.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */; };
1CD9894C1ECFFCFC0014BBBF /* BGMHermes.m in Sources */ = {isa = PBXBuildFile; fileRef = 279F48761DD6D73900768A85 /* BGMHermes.m */; };
1CD9894D1ECFFCFC0014BBBF /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; };
1CD9894E1ECFFCFC0014BBBF /* BGMVOX.m in Sources */ = {isa = PBXBuildFile; fileRef = 273F10DE1CC3D0B900C1C6DA /* BGMVOX.m */; };
1CD9894F1ECFFCFC0014BBBF /* BGMPreferencesMenu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A71BF1B029004F4CF5 /* BGMPreferencesMenu.mm */; };
1CD989501ECFFCFC0014BBBF /* BGMAboutPanel.m in Sources */ = {isa = PBXBuildFile; fileRef = 27D1D6BA1DD7226C0049E707 /* BGMAboutPanel.m */; };
1CD989511ECFFCFC0014BBBF /* BGMAutoPauseMusicPrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C0BD0A41BF1A8E6004F4CF5 /* BGMAutoPauseMusicPrefs.mm */; };
1CD989521ECFFCFC0014BBBF /* BGMOutputDevicePrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */; };
1CD989531ECFFCFC0014BBBF /* BGMDeviceControlSync.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */; };
1CD989541ECFFCFC0014BBBF /* BGMPlayThrough.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */; };
1CD989551ECFFCFC0014BBBF /* BGMUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 2743C9F01D853FBB0089613B /* BGMUserDefaults.m */; };
1CD989561ECFFCFC0014BBBF /* BGMXPCListener.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2795973A1C982E4E00A002FB /* BGMXPCListener.mm */; };
1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963071BCAF677008A4DF7 /* CAHostTimeBase.cpp */; };
1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1963041BCAF468008A4DF7 /* CAMutex.cpp */; };
1CD989591ECFFD250014BBBF /* CAPThread.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034C21BDAFD5700668E00 /* CAPThread.cpp */; };
1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C1962E21BC94E15008A4DF7 /* CARingBuffer.cpp */; };
1CE7064C1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CE7064B1BF1EC0600BFC06D /* BGMOutputDevicePrefs.mm */; };
1CED61691C3081C2002CAFCF /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 1CED61681C3081C2002CAFCF /* LICENSE */; };
1CED616C1C316E1A002CAFCF /* BGMAudioDeviceManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */; };
1CF5423C1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
1CF5423D1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */; };
270A84511E0044EF00F13C99 /* ScriptingBridge.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 270A84501E0044EE00F13C99 /* ScriptingBridge.framework */; };
271677BA1C6CBDFA0080B0A2 /* CACFNumber.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */; };
27379B8A1C7C562D0084A24C /* BGMVLC.m in Sources */ = {isa = PBXBuildFile; fileRef = 27379B891C7C562D0084A24C /* BGMVLC.m */; };
@ -150,17 +200,26 @@
1C2336DC1BEAB73F004C1C4E /* BGMMusicPlayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BGMMusicPlayer.h; path = "Music Players/BGMMusicPlayer.h"; sourceTree = "<group>"; };
1C2336DD1BEAE10C004C1C4E /* BGMSpotify.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMSpotify.h; path = "Music Players/BGMSpotify.h"; sourceTree = "<group>"; };
1C2336DE1BEAE10C004C1C4E /* BGMSpotify.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMSpotify.m; path = "Music Players/BGMSpotify.m"; sourceTree = "<group>"; };
1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = BGMApp.sdef; path = Scripting/BGMApp.sdef; sourceTree = "<group>"; };
1C2FC30D1EBC97DA00A76592 /* BGMApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMApp.h; path = BGMAppTests/UITests/BGMApp.h; sourceTree = SOURCE_ROOT; };
1C2FC3121EC706E000A76592 /* BGMAppDelegate+AppleScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "BGMAppDelegate+AppleScript.h"; path = "Scripting/BGMAppDelegate+AppleScript.h"; sourceTree = "<group>"; };
1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "BGMAppDelegate+AppleScript.mm"; path = "Scripting/BGMAppDelegate+AppleScript.mm"; sourceTree = "<group>"; };
1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = BGMASOutputDevice.mm; path = Scripting/BGMASOutputDevice.mm; sourceTree = "<group>"; };
1C2FC31D1EC723A100A76592 /* BGMASOutputDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BGMASOutputDevice.h; path = Scripting/BGMASOutputDevice.h; sourceTree = "<group>"; };
1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMDeviceControlsList.cpp; sourceTree = "<group>"; };
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlsList.h; sourceTree = "<group>"; };
1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppVolumes.mm; sourceTree = "<group>"; };
1C3DB48A1BE0888500EC8160 /* BGMAppVolumes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAppVolumes.h; sourceTree = "<group>"; };
1C4699461BD5C0E400F78043 /* BGMiTunes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BGMiTunes.m; path = "Music Players/BGMiTunes.m"; sourceTree = "<group>"; };
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMDeviceControlSync.cpp; sourceTree = "<group>"; };
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMDeviceControlSync.h; sourceTree = "<group>"; };
1C50FF641EC9F4500031A6EA /* libPublicUtility.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libPublicUtility.a; path = "../../../Library/Developer/Xcode/DerivedData/BGM-cgeucfvbrkmtbnhewbqjwrqspirp/Build/Products/Debug/libPublicUtility.a"; sourceTree = "<group>"; };
1C8034C21BDAFD5700668E00 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
1C8034C31BDAFD5700668E00 /* CAPThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPThread.h; path = PublicUtility/CAPThread.h; sourceTree = "<group>"; };
1CB8B3361BBA75EF000E2DD1 /* Background Music.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Background Music.app"; sourceTree = BUILT_PRODUCTS_DIR; };
1CB8B33A1BBA75EF000E2DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1CB8B33B1BBA75EF000E2DD1 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
1CB8B33C1BBA75EF000E2DD1 /* AppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.mm; sourceTree = "<group>"; };
1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BGMAppDelegate.h; sourceTree = "<group>"; };
1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAppDelegate.mm; sourceTree = "<group>"; };
1CB8B33E1BBA75EF000E2DD1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
1CB8B3431BBA75EF000E2DD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
1CC1DF7D1BE5068A00FB8FE4 /* CACFArray.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFArray.cpp; path = PublicUtility/CACFArray.cpp; sourceTree = "<group>"; };
@ -184,6 +243,8 @@
1CED61681C3081C2002CAFCF /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDeviceManager.h; sourceTree = "<group>"; };
1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = BGMAudioDeviceManager.mm; sourceTree = "<group>"; };
1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGMAudioDevice.cpp; sourceTree = "<group>"; };
1CF5423B1EAAEE4300445AD8 /* BGMAudioDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGMAudioDevice.h; sourceTree = "<group>"; };
1CF69BA41BCFF59C009B5D1F /* BGMApp.profdata */ = {isa = PBXFileReference; lastKnownFileType = file; path = BGMApp.profdata; sourceTree = "<group>"; };
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; };
271677B81C6CBDFA0080B0A2 /* CACFNumber.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFNumber.cpp; path = PublicUtility/CACFNumber.cpp; sourceTree = "<group>"; };
@ -251,6 +312,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1CD989401ECFFCC50014BBBF /* AudioToolbox.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -338,6 +400,18 @@
name = PublicUtility;
sourceTree = "<group>";
};
1C2FC3161EC7078F00A76592 /* Scripting */ = {
isa = PBXGroup;
children = (
1C2FC3121EC706E000A76592 /* BGMAppDelegate+AppleScript.h */,
1C2FC3131EC706E000A76592 /* BGMAppDelegate+AppleScript.mm */,
1C2FC31D1EC723A100A76592 /* BGMASOutputDevice.h */,
1C2FC31A1EC7238A00A76592 /* BGMASOutputDevice.mm */,
1C2FC2FF1EB4D6E700A76592 /* BGMApp.sdef */,
);
name = Scripting;
sourceTree = "<group>";
};
1C4699401BD5BA1700F78043 /* SharedSource */ = {
isa = PBXGroup;
children = (
@ -406,12 +480,14 @@
1CB8B3381BBA75EF000E2DD1 /* BGMApp */ = {
isa = PBXGroup;
children = (
1CB8B33B1BBA75EF000E2DD1 /* AppDelegate.h */,
1CB8B33C1BBA75EF000E2DD1 /* AppDelegate.mm */,
1CB8B33B1BBA75EF000E2DD1 /* BGMAppDelegate.h */,
1CB8B33C1BBA75EF000E2DD1 /* BGMAppDelegate.mm */,
1C3DB48A1BE0888500EC8160 /* BGMAppVolumes.h */,
1C3DB4881BE0885A00EC8160 /* BGMAppVolumes.mm */,
1CED616A1C316E1A002CAFCF /* BGMAudioDeviceManager.h */,
1CED616B1C316E1A002CAFCF /* BGMAudioDeviceManager.mm */,
1CF5423B1EAAEE4300445AD8 /* BGMAudioDevice.h */,
1CF5423A1EAAEE4300445AD8 /* BGMAudioDevice.cpp */,
27C457E41CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.h */,
27C457E51CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m */,
1C1465B91BCC49D1003AEFE6 /* BGMAutoPauseMusic.h */,
@ -420,12 +496,15 @@
1C0BD0A21BF1A827004F4CF5 /* Preferences Menu */,
1C46994D1BD7694C00F78043 /* BGMDeviceControlSync.h */,
1C46994C1BD7694C00F78043 /* BGMDeviceControlSync.cpp */,
1C3D36711ED90E8600F98E66 /* BGMDeviceControlsList.h */,
1C3D36701ED90E8600F98E66 /* BGMDeviceControlsList.cpp */,
1C1962E61BC94E91008A4DF7 /* BGMPlayThrough.h */,
1C1962E51BC94E91008A4DF7 /* BGMPlayThrough.cpp */,
2743C9ED1D8538700089613B /* BGMUserDefaults.h */,
2743C9F01D853FBB0089613B /* BGMUserDefaults.m */,
2795973C1C982E8C00A002FB /* BGMXPCListener.h */,
2795973A1C982E4E00A002FB /* BGMXPCListener.mm */,
1C2FC3161EC7078F00A76592 /* Scripting */,
1CB8B3421BBA75EF000E2DD1 /* MainMenu.xib */,
1CB8B3391BBA75EF000E2DD1 /* Supporting Files */,
);
@ -494,6 +573,7 @@
1CCC4F551E584081008053E4 /* UI Tests */ = {
isa = PBXGroup;
children = (
1C2FC30D1EBC97DA00A76592 /* BGMApp.h */,
1CCC4F611E584100008053E4 /* BGMAppUITests.m */,
);
name = "UI Tests";
@ -538,6 +618,7 @@
2743CA1B1D86DA9B0089613B /* Frameworks */ = {
isa = PBXGroup;
children = (
1C50FF641EC9F4500031A6EA /* libPublicUtility.a */,
270A84501E0044EE00F13C99 /* ScriptingBridge.framework */,
1CD1FD2F1BDDEAF2004F7E1B /* AudioToolbox.framework */,
1C1963021BCAC160008A4DF7 /* CoreAudio.framework */,
@ -657,6 +738,9 @@
isa = PBXProject;
attributes = {
CLASSPREFIX = BGM;
KnownAssetTags = (
New,
);
LastUpgradeCheck = 0820;
ORGANIZATIONNAME = "Background Music contributors";
TargetAttributes = {
@ -712,6 +796,7 @@
files = (
274827951E11052500B31D8D /* MainMenu.xib in Resources */,
1CED61691C3081C2002CAFCF /* LICENSE in Resources */,
1C2FC3041EB4D6E700A76592 /* BGMApp.sdef in Resources */,
1CC1DF961BE8607700FB8FE4 /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -801,8 +886,10 @@
1C46994E1BD7694C00F78043 /* BGMDeviceControlSync.cpp in Sources */,
2743C9EB1D852B360089613B /* BGMMusicPlayers.mm in Sources */,
1C1963091BCAF677008A4DF7 /* CAHostTimeBase.cpp in Sources */,
1C2FC3141EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */,
1C2FC31B1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */,
1CB8B33F1BBA75EF000E2DD1 /* main.m in Sources */,
1CB8B33D1BBA75EF000E2DD1 /* AppDelegate.mm in Sources */,
1CB8B33D1BBA75EF000E2DD1 /* BGMAppDelegate.mm in Sources */,
271677BA1C6CBDFA0080B0A2 /* CACFNumber.cpp in Sources */,
27D1D6BB1DD7226C0049E707 /* BGMAboutPanel.m in Sources */,
27379B8A1C7C562D0084A24C /* BGMVLC.m in Sources */,
@ -816,7 +903,9 @@
1C1962FA1BCAC061008A4DF7 /* CADebugMacros.cpp in Sources */,
27FB8C2F1DE468320084DB9D /* BGM_Utils.cpp in Sources */,
1C1962F31BCABFC5008A4DF7 /* CAHALAudioDevice.cpp in Sources */,
1CF5423C1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */,
1CC1DF911BE5891300FB8FE4 /* CADebugger.cpp in Sources */,
1C3D36721ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
2795973B1C982E4E00A002FB /* BGMXPCListener.mm in Sources */,
27C457E61CF2BC2600A6C9A6 /* BGMAutoPauseMenuItem.m in Sources */,
1C1465B81BCC3A73003AEFE6 /* BGMAutoPauseMusic.mm in Sources */,
@ -827,7 +916,49 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1CD989571ECFFD250014BBBF /* CAHostTimeBase.cpp in Sources */,
1CD989581ECFFD250014BBBF /* CAMutex.cpp in Sources */,
1CD989591ECFFD250014BBBF /* CAPThread.cpp in Sources */,
1CD9895A1ECFFD250014BBBF /* CARingBuffer.cpp in Sources */,
1CD989421ECFFCFC0014BBBF /* BGMAppVolumes.mm in Sources */,
1CD989431ECFFCFC0014BBBF /* BGMAudioDeviceManager.mm in Sources */,
1CD989441ECFFCFC0014BBBF /* BGMAutoPauseMenuItem.m in Sources */,
1CD989451ECFFCFC0014BBBF /* BGMAutoPauseMusic.mm in Sources */,
1CD989461ECFFCFC0014BBBF /* BGMMusicPlayers.mm in Sources */,
1CD989471ECFFCFC0014BBBF /* BGMScriptingBridge.m in Sources */,
1CD989481ECFFCFC0014BBBF /* BGMMusicPlayer.m in Sources */,
1CD989491ECFFCFC0014BBBF /* BGMDecibel.m in Sources */,
1C3D36731ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
1CD9894A1ECFFCFC0014BBBF /* BGMiTunes.m in Sources */,
1CD9894B1ECFFCFC0014BBBF /* BGMSpotify.m in Sources */,
1CD9894C1ECFFCFC0014BBBF /* BGMHermes.m in Sources */,
1CD9894D1ECFFCFC0014BBBF /* BGMVLC.m in Sources */,
1CD9894E1ECFFCFC0014BBBF /* BGMVOX.m in Sources */,
1CD9894F1ECFFCFC0014BBBF /* BGMPreferencesMenu.mm in Sources */,
1CD989501ECFFCFC0014BBBF /* BGMAboutPanel.m in Sources */,
1CD989511ECFFCFC0014BBBF /* BGMAutoPauseMusicPrefs.mm in Sources */,
1CD989521ECFFCFC0014BBBF /* BGMOutputDevicePrefs.mm in Sources */,
1CD989531ECFFCFC0014BBBF /* BGMDeviceControlSync.cpp in Sources */,
1CD989541ECFFCFC0014BBBF /* BGMPlayThrough.cpp in Sources */,
1CD989551ECFFCFC0014BBBF /* BGMUserDefaults.m in Sources */,
1CD989561ECFFCFC0014BBBF /* BGMXPCListener.mm in Sources */,
1CD989411ECFFCD10014BBBF /* BGMAppDelegate.mm in Sources */,
1CD989341ECFFC9E0014BBBF /* BGM_Utils.cpp in Sources */,
1CD989351ECFFC9E0014BBBF /* CACFArray.cpp in Sources */,
1CD989361ECFFC9E0014BBBF /* CACFDictionary.cpp in Sources */,
1CD989371ECFFC9E0014BBBF /* CACFNumber.cpp in Sources */,
1CD989381ECFFC9E0014BBBF /* CACFString.cpp in Sources */,
1CD989391ECFFC9E0014BBBF /* CADebugger.cpp in Sources */,
1CD9893A1ECFFC9E0014BBBF /* CADebugMacros.cpp in Sources */,
1CD9893B1ECFFC9E0014BBBF /* CADebugPrintf.cpp in Sources */,
1CD9893C1ECFFC9E0014BBBF /* CAHALAudioDevice.cpp in Sources */,
1CD9893D1ECFFC9E0014BBBF /* CAHALAudioObject.cpp in Sources */,
1CD9893E1ECFFC9E0014BBBF /* CAHALAudioStream.cpp in Sources */,
1CD9893F1ECFFC9E0014BBBF /* CAHALAudioSystemObject.cpp in Sources */,
1C50FF631EC9F4490031A6EA /* BGMAudioDevice.cpp in Sources */,
1CCC4F621E584100008053E4 /* BGMAppUITests.m in Sources */,
1C2FC31C1EC7238A00A76592 /* BGMASOutputDevice.mm in Sources */,
1C2FC3151EC706E000A76592 /* BGMAppDelegate+AppleScript.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -845,6 +976,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1C3D36741ED90E8600F98E66 /* BGMDeviceControlsList.cpp in Sources */,
27FB8C301DE4758A0084DB9D /* BGMPlayThrough.cpp in Sources */,
27FB8C311DE4758A0084DB9D /* BGM_Utils.cpp in Sources */,
27FB8C071DD75D0A0084DB9D /* BGMHermes.m in Sources */,
@ -871,6 +1003,7 @@
2743CA051D86D41C0089613B /* BGMDecibel.m in Sources */,
2743CA061D86D41C0089613B /* BGMSpotify.m in Sources */,
2743CA071D86D41C0089613B /* BGMVLC.m in Sources */,
1CF5423D1EAAEE4300445AD8 /* BGMAudioDevice.cpp in Sources */,
2743CA081D86D41C0089613B /* BGMVOX.m in Sources */,
2743CA091D86D41C0089613B /* BGMUserDefaults.m in Sources */,
2743CA011D86D3CB0089613B /* BGMMusicPlayers.mm in Sources */,
@ -959,10 +1092,10 @@
"CoreAudio_Debug=1",
"CoreAudio_UseSysLog=1",
"CoreAudio_StopOnAssert=1",
"CoreAudio_ThreadStampMessages=1",
"CoreAudio_ThreadStampMessages=0",
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
"CoreAudio_StopOnThrow=0",
"CoreAudio_StopOnThrow=1",
);
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
@ -1054,10 +1187,10 @@
"CoreAudio_Debug=1",
"CoreAudio_UseSysLog=1",
"CoreAudio_StopOnAssert=1",
"CoreAudio_ThreadStampMessages=1",
"CoreAudio_ThreadStampMessages=0",
"BGM_StopDebuggerOnLoggedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_EXCEPTIONS)",
"BGM_StopDebuggerOnLoggedUnexpectedExceptions=$(BGM_STOP_DEBUGGER_ON_LOGGED_UNEXPECTED_EXCEPTIONS)",
"CoreAudio_StopOnThrow=0",
"CoreAudio_StopOnThrow=1",
);
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;

View file

@ -26,8 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
enableAddressSanitizer = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
@ -70,6 +69,7 @@
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
enableAddressSanitizer = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "NO">
<BuildableProductRunnable
@ -85,7 +85,7 @@
<EnvironmentVariables>
<EnvironmentVariable
key = "ASAN_OPTIONS"
value = "detect_odr_violation=0"
value = "detect_odr_violation=0,use_odr_indicator=1"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>

View file

@ -14,7 +14,7 @@
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// AppDelegate.h
// BGMAppDelegate.h
// BGMApp
//
// Copyright © 2016 Kyle Neideck
@ -22,11 +22,14 @@
// Sets up and tears down the app.
//
// Local Includes
#import "BGMAudioDeviceManager.h"
// System Includes
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <NSApplicationDelegate, NSMenuDelegate>
@interface BGMAppDelegate : NSObject <NSApplicationDelegate, NSMenuDelegate>
@property (weak) IBOutlet NSMenu* bgmMenu;
@property (weak) IBOutlet NSView* appVolumeView;
@ -34,5 +37,7 @@
@property (unsafe_unretained) IBOutlet NSTextView* aboutPanelLicenseView;
@property (weak) IBOutlet NSMenuItem* autoPauseMenuItemUnwrapped;
@property (readonly) BGMAudioDeviceManager* audioDevices;
@end

View file

@ -14,19 +14,18 @@
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// AppDelegate.mm
// BGMAppDelegate.mm
// BGMApp
//
// Copyright © 2016, 2017 Kyle Neideck
//
// Self Includes
#import "AppDelegate.h"
#import "BGMAppDelegate.h"
// Local Includes
#import "BGM_Types.h"
#import "BGMUserDefaults.h"
#import "BGMAudioDeviceManager.h"
#import "BGMMusicPlayers.h"
#import "BGMAutoPauseMusic.h"
#import "BGMAutoPauseMenuItem.h"
@ -40,7 +39,7 @@
static float const kStatusBarIconPadding = 0.25;
@implementation AppDelegate {
@implementation BGMAppDelegate {
// The button in the system status bar (the bar with volume, battery, clock, etc.) to show the main menu
// for the app. These are called "menu bar extras" in the Human Interface Guidelines.
NSStatusItem* statusBarItem;
@ -52,11 +51,12 @@ static float const kStatusBarIconPadding = 0.25;
BGMAutoPauseMenuItem* autoPauseMenuItem;
BGMMusicPlayers* musicPlayers;
BGMAppVolumes* appVolumes;
BGMAudioDeviceManager* audioDevices;
BGMPreferencesMenu* prefsMenu;
BGMXPCListener* xpcListener;
}
@synthesize audioDevices = audioDevices;
- (void) awakeFromNib {
// Show BGMApp in the dock, if the command-line option for that was passed. This is used by the UI tests.
if ([NSProcessInfo.processInfo.arguments indexOfObject:@"--show-dock-icon"] != NSNotFound) {
@ -134,7 +134,7 @@ static float const kStatusBarIconPadding = 0.25;
xpcListener = [[BGMXPCListener alloc] initWithAudioDevices:audioDevices
helperConnectionErrorHandler:^(NSError* error) {
NSLog(@"AppDelegate::applicationDidFinishLaunching: (helperConnectionErrorHandler) "
NSLog(@"BGMAppDelegate::applicationDidFinishLaunching: (helperConnectionErrorHandler) "
"BGMXPCHelper connection error: %@",
error);
@ -164,7 +164,7 @@ static float const kStatusBarIconPadding = 0.25;
- (void) applicationWillTerminate:(NSNotification*)aNotification {
#pragma unused (aNotification)
DebugMsg("AppDelegate::applicationWillTerminate");
DebugMsg("BGMAppDelegate::applicationWillTerminate");
NSError* error = [audioDevices unsetBGMDeviceAsOSDefault];
@ -254,16 +254,16 @@ static float const kStatusBarIconPadding = 0.25;
// In System Preferences, go to the "Output" tab on the "Sound" pane.
for (SystemPreferencesPane* pane : [sysPrefs panes]) {
DebugMsg("AppDelegate::openSysPrefsSoundOutput: pane = %s", [pane.name UTF8String]);
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("AppDelegate::openSysPrefsSoundOutput: anchor = %s", [anchor.name UTF8String]);
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: anchor = %s", [anchor.name UTF8String]);
if ([[anchor.name lowercaseString] isEqualToString:@"output"]) {
DebugMsg("AppDelegate::openSysPrefsSoundOutput: Showing Output in Sound pane.");
DebugMsg("BGMAppDelegate::openSysPrefsSoundOutput: Showing Output in Sound pane.");
[anchor reveal];
}
@ -281,7 +281,7 @@ static float const kStatusBarIconPadding = 0.25;
if ([menu isEqual:self.bgmMenu]) {
[autoPauseMenuItem parentMenuNeedsUpdate];
} else {
DebugMsg("AppDelegate::menuNeedsUpdate: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
DebugMsg("BGMAppDelegate::menuNeedsUpdate: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
}
}
@ -289,7 +289,7 @@ static float const kStatusBarIconPadding = 0.25;
if ([menu isEqual:self.bgmMenu]) {
[autoPauseMenuItem parentMenuItemWillHighlight:item];
} else {
DebugMsg("AppDelegate::menu: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
DebugMsg("BGMAppDelegate::menu: Warning: unexpected menu. menu=%s", menu.description.UTF8String);
}
}

View file

@ -121,6 +121,9 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
// Set the slider to the volume for this app if we got one from the driver
[self setVolumeOfMenuItem:appVolItem fromAppVolumes:appVolumesOnDevice];
// TODO: This doesn't show up in Accessibility Inspector for me. Not sure why.
appVolItem.accessibilityTitle = [NSString stringWithFormat:@"%@", [app localizedName]];
[bgmMenu insertItem:appVolItem atIndex:index];
}
@ -356,6 +359,8 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
// 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];
self.accessibilityTitle = @"More options";
}
@end
@ -380,6 +385,8 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
self.maxValue = kAppRelativeVolumeMaxRawValue;
self.minValue = kAppRelativeVolumeMinRawValue;
self.accessibilityTitle = [NSString stringWithFormat:@"Volume for %@", [app localizedName]];
}
// We have to handle snapping for volume sliders ourselves because adding a tick mark (snap point) in Interface Builder
@ -429,6 +436,8 @@ static CGFloat const kAppVolumeViewInitialHeight = 20;
self.minValue = kAppPanLeftRawValue;
self.maxValue = kAppPanRightRawValue;
self.accessibilityTitle = [NSString stringWithFormat:@"Pan for %@", [app localizedName]];
}
- (void) setPanPosition:(NSNumber *)panPosition {

View file

@ -0,0 +1,366 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMAudioDevice.cpp
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
// Self Include
#include "BGMAudioDevice.h"
// Local Includes
#include "BGM_Types.h"
// System Includes
#include <AudioToolbox/AudioServices.h>
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the
// master element."
static const AudioObjectPropertyElement kMasterChannel = 0;
#pragma mark Construction/Destruction
BGMAudioDevice::BGMAudioDevice(AudioObjectID inAudioDevice)
:
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();
bool isBGMDevice = CFEqual(uid, CFSTR(kBGMDeviceUID));
bool isNullDevice = CFEqual(uid, CFSTR(kBGMNullDeviceUID));
CFRelease(uid);
bool hasOutputChannels = GetTotalNumberChannels(/* inIsInput = */ false) > 0;
bool canBeDefault = CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
return !isBGMDevice && !isNullDevice && !IsHidden() && hasOutputChannels && canBeDefault;
}
#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_VirtualMasterVolume,
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_VirtualMasterVolume,
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_VirtualMasterVolume,
inScope,
kAudioObjectPropertyElementMaster
};
didSetVolume = (kAudioServicesNoError == AHSSetPropertyData(GetObjectID(),
&virtualMasterVolumeAddress,
sizeof(Float32),
&inVolume));
// Reset the balance
AudioObjectPropertyAddress virtualMasterBalanceAddress = {
kAudioHardwareServiceDeviceProperty_VirtualMasterBalance,
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_VirtualMasterBalance,
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);
}
// 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
}

View file

@ -0,0 +1,93 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
// BGMAudioDevice.h
// BGMApp
//
// Copyright © 2017 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);
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(); }
/*! @throws CAException */
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;
private:
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 */

View file

@ -49,7 +49,12 @@ extern int const kBGMErrorCode_OutputDeviceNotFound;
- (NSError* __nullable) unsetBGMDeviceAsOSDefault;
#ifdef __cplusplus
// The virtual device published by BGMDriver.
- (CAHALAudioDevice) 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;

View file

@ -28,6 +28,7 @@
#include "BGM_Utils.h"
#include "BGMDeviceControlSync.h"
#include "BGMPlayThrough.h"
#include "BGMAudioDevice.h"
// PublicUtility Includes
#include "CAHALAudioSystemObject.h"
@ -37,13 +38,6 @@
int const kBGMErrorCode_BGMDeviceNotFound = 0;
int const kBGMErrorCode_OutputDeviceNotFound = 1;
// Hack/workaround that adds a default constructor to CAHALAudioDevice so we don't have to use pointers for the instance variables
class BGMAudioDevice : public CAHALAudioDevice {
using CAHALAudioDevice::CAHALAudioDevice;
public:
BGMAudioDevice() : CAHALAudioDevice(kAudioDeviceUnknown) { }
};
@implementation BGMAudioDeviceManager {
BGMAudioDevice bgmDevice;
BGMAudioDevice outputDevice;
@ -75,7 +69,7 @@ public:
[self initOutputDevice];
if (outputDevice.GetObjectID() == kAudioDeviceUnknown) {
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
LogError("BGMAudioDeviceManager::initWithError: output device not found");
if (error) {
@ -143,7 +137,7 @@ public:
assert(outputDevice.GetObjectID() != bgmDevice.GetObjectID());
// Log message
if (outputDevice.GetObjectID() == kAudioDeviceUnknown) {
if (outputDevice.GetObjectID() == kAudioObjectUnknown) {
CFStringRef outputDeviceUID = outputDevice.CopyDeviceUID();
DebugMsg("BGMAudioDeviceManager::initDevices: Set output device to %s",
CFStringGetCStringPtr(outputDeviceUID, kCFStringEncodingUTF8));
@ -159,26 +153,25 @@ public:
- (NSError* __nullable) setBGMDeviceAsOSDefault {
DebugMsg("BGMAudioDeviceManager::setBGMDeviceAsOSDefault: Setting the system's default audio "
"device to BGMDevice");
CAHALAudioSystemObject audioSystem;
AudioDeviceID bgmDeviceID = kAudioDeviceUnknown;
AudioDeviceID outputDeviceID = kAudioDeviceUnknown;
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
@try {
[stateLock lock];
BGMLogAndSwallowExceptions("setBGMDeviceAsOSDefault", [&]() {
bgmDeviceID = bgmDevice.GetObjectID();
outputDeviceID = outputDevice.GetObjectID();
});
bgmDeviceID = bgmDevice.GetObjectID();
outputDeviceID = outputDevice.GetObjectID();
} @finally {
[stateLock unlock];
}
if (outputDeviceID == kAudioDeviceUnknown) {
if (outputDeviceID == kAudioObjectUnknown) {
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
}
if (bgmDeviceID == kAudioDeviceUnknown) {
if (bgmDeviceID == kAudioObjectUnknown) {
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
}
@ -210,15 +203,16 @@ public:
bool bgmDeviceIsDefault = true;
bool bgmDeviceIsSystemDefault = true;
AudioDeviceID bgmDeviceID = kAudioDeviceUnknown;
AudioDeviceID outputDeviceID = kAudioDeviceUnknown;
AudioDeviceID bgmDeviceID = kAudioObjectUnknown;
AudioDeviceID outputDeviceID = kAudioObjectUnknown;
@try {
[stateLock lock];
bgmDeviceID = bgmDevice.GetObjectID();
outputDeviceID = outputDevice.GetObjectID();
BGMLogAndSwallowExceptions("unsetBGMDeviceAsOSDefault", [&]() {
bgmDeviceID = bgmDevice.GetObjectID();
outputDeviceID = outputDevice.GetObjectID();
bgmDeviceIsDefault =
(audioSystem.GetDefaultAudioDevice(false, false) == bgmDeviceID);
@ -229,10 +223,10 @@ public:
[stateLock unlock];
}
if (outputDeviceID == kAudioDeviceUnknown) {
if (outputDeviceID == kAudioObjectUnknown) {
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_OutputDeviceNotFound userInfo:nil];
}
if (bgmDeviceID == kAudioDeviceUnknown) {
if (bgmDeviceID == kAudioObjectUnknown) {
return [NSError errorWithDomain:@kBGMAppBundleID code:kBGMErrorCode_BGMDeviceNotFound userInfo:nil];
}
@ -275,6 +269,10 @@ public:
return bgmDevice;
}
- (CAHALAudioDevice) outputDevice {
return outputDevice;
}
- (BOOL) isOutputDevice:(AudioObjectID)deviceID {
@try {
[stateLock lock];
@ -339,12 +337,17 @@ public:
currentDeviceID = outputDevice.GetObjectID();
if (newDeviceID != currentDeviceID) {
// Mirror changes in BGMDevice's controls to the new output device's.
deviceControlSync = BGMDeviceControlSync(bgmDevice, newOutputDevice);
// Deactivate playthrough rather than stopping it so it can't be started by HAL
// notifications while we're updating deviceControlSync.
playThrough.Deactivate();
deviceControlSync.SetDevices(bgmDevice, newOutputDevice);
deviceControlSync.Activate();
// Stream audio from BGMDevice to the new output device. This blocks while the old device
// stops IO.
playThrough.SetDevices(&bgmDevice, &newOutputDevice);
playThrough.Activate();
outputDevice = newOutputDevice;
}
@ -387,7 +390,7 @@ public:
BGMLogAndSwallowExceptions("BGMAudioDeviceManager::setDataSource", [&]() {
AudioObjectPropertyScope scope = kAudioObjectPropertyScopeOutput;
UInt32 channel = 0;
if (device.DataSourceControlIsSettable(scope, channel)) {
DebugMsg("BGMAudioDeviceManager::setOutputDeviceWithID: Setting dataSourceID=%u",
dataSourceID);
@ -455,7 +458,7 @@ public:
// notified by the HAL yet.
LogWarning("BGMAudioDeviceManager::waitForOutputDeviceToStart: Playthrough wasn't starting the "
"output device. Will tell it to and then return early with kDeviceNotStarting.");
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
@try {
[stateLock lock];

View file

@ -190,7 +190,7 @@ static SInt64 const kMenuItemUpdateWaitTime = 1;
// 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) {
if (willHighlightMenuItem != menuItem.highlighted) {
[self updateMenuItemTitleWithHighlight:willHighlightMenuItem];
}
}

View file

@ -17,7 +17,7 @@
// BGMDeviceControlSync.cpp
// BGMApp
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
//
// Self Include
@ -27,15 +27,12 @@
#include "BGM_Types.h"
#include "BGM_Utils.h"
// System Includes
#include <AudioToolbox/AudioServices.h>
// PublicUtility Includes
#include "CAPropertyAddress.h"
#pragma clang assume_nonnull begin
// AudioObjectPropertyElement docs: "Elements are numbered sequentially where 0 represents the master element."
static const AudioObjectPropertyElement kMasterChannel = 0;
static const AudioObjectPropertyAddress kMutePropertyAddress =
{ kAudioDevicePropertyMute, kAudioObjectPropertyScopeOutput, kAudioObjectPropertyElementMaster };
@ -44,31 +41,55 @@ static const AudioObjectPropertyAddress kVolumePropertyAddress =
#pragma mark Construction/Destruction
BGMDeviceControlSync::BGMDeviceControlSync(CAHALAudioDevice inBGMDevice, CAHALAudioDevice inOutputDevice)
BGMDeviceControlSync::BGMDeviceControlSync(AudioObjectID inBGMDevice,
AudioObjectID inOutputDevice,
CAHALAudioSystemObject inAudioSystem)
:
mBGMDevice(inBGMDevice),
mOutputDevice(inOutputDevice)
mOutputDevice(inOutputDevice),
mAudioSystem(inAudioSystem),
mBGMDeviceControlsList(inBGMDevice)
{
Activate();
}
BGMDeviceControlSync::~BGMDeviceControlSync()
{
Deactivate();
BGMLogAndSwallowExceptions("BGMDeviceControlSync::~BGMDeviceControlSync", [&] {
CAMutex::Locker locker(mMutex);
Deactivate();
});
}
void BGMDeviceControlSync::Activate()
{
ThrowIf((mBGMDevice.GetObjectID() == kAudioDeviceUnknown || mOutputDevice.GetObjectID() == kAudioDeviceUnknown),
CAMutex::Locker locker(mMutex);
ThrowIf((mBGMDevice.GetObjectID() == kAudioObjectUnknown || mOutputDevice.GetObjectID() == kAudioObjectUnknown),
BGM_DeviceNotSetException(),
"BGMDeviceControlSync::Activate: Both the output device and BGMDevice must be set to start synchronizing their controls");
// Init BGMDevice controls to match output device
CopyVolume(mOutputDevice, mBGMDevice, kAudioObjectPropertyScopeOutput);
CopyMute(mOutputDevice, mBGMDevice, kAudioObjectPropertyScopeOutput);
if(!mActive)
{
DebugMsg("BGMDeviceControlSync::Activate: Activating control sync");
// Disable BGMDevice controls that the output device doesn't have and reenable any that were
// disabled for the previous output device.
//
// Continue anyway if this fails because it's better to have extra/missing controls than to
// be unable to use the device.
BGMLogAndSwallowExceptionsMsg("BGMDeviceControlSync::Activate", "Controls list", [&] {
bool wasUpdated = mBGMDeviceControlsList.MatchControlsListOf(mOutputDevice);
if(wasUpdated)
{
mBGMDeviceControlsList.PropagateControlListChange();
}
});
// Init BGMDevice controls to match output device
mBGMDevice.CopyVolumeFrom(mOutputDevice, kAudioObjectPropertyScopeOutput);
mBGMDevice.CopyMuteFrom(mOutputDevice, kAudioObjectPropertyScopeOutput);
// Register listeners for volume and mute values
mBGMDevice.AddPropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
@ -87,286 +108,129 @@ void BGMDeviceControlSync::Activate()
mActive = true;
}
else
{
DebugMsg("BGMDeviceControlSync::Activate: Already active");
}
}
void BGMDeviceControlSync::Deactivate()
{
if(mActive && mBGMDevice.GetObjectID() != kAudioDeviceUnknown)
CAMutex::Locker locker(mMutex);
if(mActive)
{
// Unregister listeners
mBGMDevice.RemovePropertyListener(kVolumePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
mBGMDevice.RemovePropertyListener(kMutePropertyAddress, &BGMDeviceControlSync::BGMDeviceListenerProc, this);
DebugMsg("BGMDeviceControlSync::Deactivate: Deactivating control sync");
// Deregister listeners
if(mBGMDevice.GetObjectID() != kAudioDeviceUnknown)
{
BGMLogAndSwallowExceptions("BGMDeviceControlSync::Deactivate", [&] {
mBGMDevice.RemovePropertyListener(kVolumePropertyAddress,
&BGMDeviceControlSync::BGMDeviceListenerProc,
this);
});
BGMLogAndSwallowExceptions("BGMDeviceControlSync::Deactivate", [&] {
mBGMDevice.RemovePropertyListener(kMutePropertyAddress,
&BGMDeviceControlSync::BGMDeviceListenerProc,
this);
});
}
mActive = false;
}
else
{
DebugMsg("BGMDeviceControlSync::Deactivate: Not active");
}
}
#pragma mark Accessors
void BGMDeviceControlSync::Swap(BGMDeviceControlSync& inDeviceControlSync)
void BGMDeviceControlSync::SetDevices(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice)
{
mBGMDevice = inDeviceControlSync.mBGMDevice;
mOutputDevice = inDeviceControlSync.mOutputDevice;
CAMutex::Locker locker(mMutex);
bool wasActive = mActive;
Deactivate();
mBGMDevice = inBGMDevice;
mBGMDeviceControlsList.SetBGMDevice(inBGMDevice);
mOutputDevice = inOutputDevice;
BGMLogAndSwallowExceptionsMsg("BGMDeviceControlSync::Swap", "Deactivate", [&inDeviceControlSync]() {
inDeviceControlSync.Deactivate();
});
BGMLogAndSwallowExceptionsMsg("BGMDeviceControlSync::Swap", "Activate", [&]() {
if(wasActive)
{
Activate();
});
}
#pragma mark Get/Set Control Values
// static
void BGMDeviceControlSync::CopyMute(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope)
{
// TODO: Support for devices that have per-channel mute controls but no master mute control
bool toHasSettableMasterMute = inToDevice.HasMuteControl(inScope, kMasterChannel) && inToDevice.MuteControlIsSettable(inScope, kMasterChannel);
if(toHasSettableMasterMute && inFromDevice.HasMuteControl(inScope, kMasterChannel))
{
inToDevice.SetMuteControlValue(inScope,
kMasterChannel,
inFromDevice.GetMuteControlValue(inScope, kMasterChannel));
}
}
// static
void BGMDeviceControlSync::CopyVolume(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope)
{
// Get the volume of the from device
bool didGetFromVolume = false;
Float32 fromVolume = FLT_MIN;
if(inFromDevice.HasVolumeControl(inScope, kMasterChannel))
{
fromVolume = inFromDevice.GetVolumeControlScalarValue(inScope, kMasterChannel);
didGetFromVolume = true;
}
// Use the average channel volume of the from device if it has no master volume
if(!didGetFromVolume)
{
UInt32 fromNumChannels = inFromDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
fromVolume = 0;
for(UInt32 channel = 1; channel <= fromNumChannels; channel++)
{
if(inFromDevice.HasVolumeControl(inScope, channel))
{
fromVolume += inFromDevice.GetVolumeControlScalarValue(inScope, channel);
didGetFromVolume = true;
}
}
fromVolume /= fromNumChannels;
}
// Set the volume of the to device
if(didGetFromVolume && fromVolume != FLT_MIN)
{
bool didSetVolume = false;
try
{
didSetVolume = SetMasterVolumeScalar(inToDevice, inScope, fromVolume);
}
catch(CAException e)
{
OSStatus err = e.GetError();
char err4CC[5] = CA4CCToCString(err);
CFStringRef uid = inToDevice.CopyDeviceUID();
LogWarning("BGMDeviceControlSync::CopyVolume: 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 fromVirtualMasterVolume;
bool success = GetVirtualMasterVolume(inFromDevice, inScope, fromVirtualMasterVolume);
if(success)
{
didSetVolume = SetVirtualMasterVolume(inToDevice, inScope, fromVirtualMasterVolume);
}
}
if(!didSetVolume)
{
// Couldn't set a master or virtual master volume, so as a fallback try to set each channel individually
UInt32 numChannels = inToDevice.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
for(UInt32 channel = 1; channel <= numChannels; channel++)
{
if(inToDevice.HasVolumeControl(inScope, channel) && inToDevice.VolumeControlIsSettable(inScope, channel))
{
inToDevice.SetVolumeControlScalarValue(inScope, channel, fromVolume);
}
}
}
}
}
#pragma mark Listener Procs
// static
bool BGMDeviceControlSync::SetMasterVolumeScalar(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume)
OSStatus BGMDeviceControlSync::BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData)
{
bool hasSettableMasterVolume =
inDevice.HasVolumeControl(inScope, kMasterChannel) && inDevice.VolumeControlIsSettable(inScope, kMasterChannel);
if(hasSettableMasterVolume)
{
inDevice.SetVolumeControlScalarValue(inScope, kMasterChannel, inVolume);
return true;
}
return false;
}
// static
bool BGMDeviceControlSync::GetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterVolume)
{
AudioObjectPropertyAddress virtualMasterVolumeAddress =
{ kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, inScope, kAudioObjectPropertyElementMaster };
if(!AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterVolumeAddress))
{
return false;
}
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
return kAudioServicesNoError == AHSGetPropertyData(inDevice.GetObjectID(),
&virtualMasterVolumeAddress,
&virtualMasterVolumePropertySize,
&outVirtualMasterVolume);
}
// static
bool BGMDeviceControlSync::SetVirtualMasterVolume(CAHALAudioDevice inDevice, 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;
AudioObjectPropertyAddress virtualMasterVolumeAddress =
{ kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, inScope, kAudioObjectPropertyElementMaster };
bool hasVirtualMasterVolume = AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterVolumeAddress);
Boolean virtualMasterVolumeIsSettable;
OSStatus err = AudioHardwareServiceIsPropertySettable(inDevice.GetObjectID(), &virtualMasterVolumeAddress, &virtualMasterVolumeIsSettable);
virtualMasterVolumeIsSettable &= (err == kAudioServicesNoError);
if(hasVirtualMasterVolume && virtualMasterVolumeIsSettable)
{
// 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(inDevice, inScope, virtualMasterBalance);
didSetVolume = kAudioServicesNoError == AHSSetPropertyData(inDevice.GetObjectID(), &virtualMasterVolumeAddress, sizeof(Float32), &inVolume);
// Reset the balance
AudioObjectPropertyAddress virtualMasterBalanceAddress =
{ kAudioHardwareServiceDeviceProperty_VirtualMasterBalance, inScope, kAudioObjectPropertyElementMaster };
if(didSetVolume && didGetVirtualMasterBalance && AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterBalanceAddress))
{
Boolean balanceIsSettable;
err = AudioHardwareServiceIsPropertySettable(inDevice.GetObjectID(), &virtualMasterBalanceAddress, &balanceIsSettable);
if(err == kAudioServicesNoError && balanceIsSettable)
{
AHSSetPropertyData(inDevice.GetObjectID(), &virtualMasterBalanceAddress, sizeof(Float32), &virtualMasterBalance);
}
}
}
return didSetVolume;
}
// static
bool BGMDeviceControlSync::GetVirtualMasterBalance(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance)
{
AudioObjectPropertyAddress virtualMasterBalanceAddress =
{ kAudioHardwareServiceDeviceProperty_VirtualMasterBalance, inScope, kAudioObjectPropertyElementMaster };
if(!AudioHardwareServiceHasProperty(inDevice.GetObjectID(), &virtualMasterBalanceAddress))
{
return false;
}
UInt32 virtualMasterVolumePropertySize = sizeof(Float32);
return kAudioServicesNoError == AHSGetPropertyData(inDevice.GetObjectID(),
&virtualMasterBalanceAddress,
&virtualMasterVolumePropertySize,
&outVirtualMasterBalance);
}
// static
OSStatus BGMDeviceControlSync::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"
// 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 BGMDeviceControlSync::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"
return AudioHardwareServiceSetPropertyData(inObjectID, inAddress, 0, NULL, inDataSize, inData);
#pragma clang diagnostic pop
}
#pragma mark Listener
// static
OSStatus BGMDeviceControlSync::BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* __nonnull inAddresses, void* __nullable inClientData)
{
// refCon (reference context) is the instance that registered this listener proc
// refCon (reference context) is the instance that registered this listener proc.
BGMDeviceControlSync* refCon = static_cast<BGMDeviceControlSync*>(inClientData);
if(refCon->mActive)
{
ThrowIf(inObjectID != refCon->mBGMDevice.GetObjectID(),
CAException(kAudioHardwareBadObjectError),
"BGMDeviceControlSync::BGMDeviceListenerProc: notified about audio object other than BGMDevice");
for(int i = 0; i < inNumberAddresses; i++)
auto checkState = [&] {
if(!refCon)
{
AudioObjectPropertyScope scope = inAddresses[i].mScope;
switch(inAddresses[i].mSelector)
{
case kAudioDevicePropertyVolumeScalar:
// Update the output device
CopyVolume(refCon->mBGMDevice, refCon->mOutputDevice, scope);
break;
case kAudioDevicePropertyMute:
// Update the output device. Note that this also runs when you change the volume (on BGMDevice)
CopyMute(refCon->mBGMDevice, refCon->mOutputDevice, scope);
break;
default:
break;
}
LogError("BGMDeviceControlSync::BGMDeviceListenerProc: !refCon");
return false;
}
if(!refCon->mActive ||
(refCon->mBGMDevice.GetObjectID() == kAudioObjectUnknown) ||
(refCon->mOutputDevice.GetObjectID() == kAudioObjectUnknown))
{
return false;
}
if(inObjectID != refCon->mBGMDevice.GetObjectID())
{
LogError("BGMDeviceControlSync::BGMDeviceListenerProc: notified about audio object other than BGMDevice");
return false;
}
return true;
};
for(int i = 0; i < inNumberAddresses; i++)
{
AudioObjectPropertyScope scope = inAddresses[i].mScope;
switch(inAddresses[i].mSelector)
{
case kAudioDevicePropertyVolumeScalar:
{
CAMutex::Locker locker(refCon->mMutex);
// Update the output device's volume.
if(checkState())
{
refCon->mOutputDevice.CopyVolumeFrom(refCon->mBGMDevice, scope);
}
}
break;
case kAudioDevicePropertyMute:
{
CAMutex::Locker locker(refCon->mMutex);
// Update the output device's mute control. Note that this also runs when you
// change the volume (on BGMDevice).
if(checkState())
{
refCon->mOutputDevice.CopyMuteFrom(refCon->mBGMDevice, scope);
}
}
break;
}
}
// "The return value [of an AudioObjectPropertyListenerProc] is currently unused and should always be 0."
return 0;
}

View file

@ -17,65 +17,105 @@
// BGMDeviceControlSync.h
// BGMApp
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
//
// Listens for notifications that BGMDevice's controls (just volume and mute currently) have changed value, and
// copies the new values to the output device.
// Synchronises BGMDevice's controls (just volume and mute currently) with the output device's
// controls. This allows the user to control the output device normally while BGMDevice is set as
// the default device.
//
// BGMDeviceControlSync disables any BGMDevice controls that the output device doesn't also have.
// When the value of one of BGMDevice's controls is changed, BGMDeviceControlSync copies the new
// value to the output device.
//
// Thread safe.
//
#ifndef __BGMApp__BGMDeviceControlSync__
#define __BGMApp__BGMDeviceControlSync__
#ifndef BGMApp__BGMDeviceControlSync
#define BGMApp__BGMDeviceControlSync
// Local Includes
#include "BGMAudioDevice.h"
#include "BGMDeviceControlsList.h"
// PublicUtility Includes
#include "CAHALAudioDevice.h"
#include "CAHALAudioSystemObject.h"
#include "CAMutex.h"
// System Includes
#include <AudioToolbox/AudioServices.h>
#pragma clang assume_nonnull begin
class BGMDeviceControlSync
{
#pragma mark Construction/Destruction
public:
BGMDeviceControlSync(CAHALAudioDevice inBGMDevice, CAHALAudioDevice inOutputDevice);
BGMDeviceControlSync(AudioObjectID inBGMDevice,
AudioObjectID inOutputDevice,
CAHALAudioSystemObject inAudioSystem
= CAHALAudioSystemObject());
~BGMDeviceControlSync();
// Disallow copying
BGMDeviceControlSync(const BGMDeviceControlSync&) = delete;
BGMDeviceControlSync& operator=(const BGMDeviceControlSync&) = delete;
// Move constructor/assignment
BGMDeviceControlSync(BGMDeviceControlSync&& inDeviceControlSync) { Swap(inDeviceControlSync); }
BGMDeviceControlSync& operator=(BGMDeviceControlSync&& inDeviceControlSync) { Swap(inDeviceControlSync); return *this; }
#ifdef __OBJC__
// Only intended as a convenience for Objective-C instance vars
BGMDeviceControlSync() { };
BGMDeviceControlSync()
: BGMDeviceControlSync(kAudioObjectUnknown, kAudioObjectUnknown) { };
#endif
private:
/*!
Begin synchronising BGMDevice's controls with the output device's.
@throws BGM_DeviceNotSetException if BGMDevice isn't set.
@throws CAException if the HAL or one of the devices returns an error when this function
registers for device property notifications or when it copies the current
values of the output device's controls to BGMDevice. This
BGMDeviceControlSync will remain inactive if this function throws.
*/
void Activate();
/*! Stop synchronising BGMDevice's controls with the output device's. */
void Deactivate();
void Swap(BGMDeviceControlSync& inDeviceControlSync);
static void CopyMute(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope);
static void CopyVolume(CAHALAudioDevice inFromDevice, CAHALAudioDevice inToDevice, AudioObjectPropertyScope inScope);
static bool SetMasterVolumeScalar(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume);
static bool GetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterVolume);
static bool SetVirtualMasterVolume(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32 inVolume);
static bool GetVirtualMasterBalance(CAHALAudioDevice inDevice, AudioObjectPropertyScope inScope, Float32& outVirtualMasterBalance);
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);
static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID, UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses, void* __nullable inClientData);
#pragma mark Accessors
/*!
Set the IDs of BGMDevice and the output device to synchronise with.
@throws BGM_DeviceNotSetException if BGMDevice isn't set.
@throws CAException if the HAL or one of the new devices returns an error while restarting
synchronisation. This BGMDeviceControlSync will be deactivated if this
function throws, but its devices will still be set.
*/
void SetDevices(AudioObjectID inBGMDevice, AudioObjectID inOutputDevice);
#pragma mark Listener Procs
private:
bool mActive = false;
CAHALAudioDevice mBGMDevice { kAudioDeviceUnknown };
CAHALAudioDevice mOutputDevice { kAudioDeviceUnknown };
/*! Receives HAL notifications about the BGMDevice properties this class listens to. */
static OSStatus BGMDeviceListenerProc(AudioObjectID inObjectID,
UInt32 inNumberAddresses,
const AudioObjectPropertyAddress* inAddresses,
void* __nullable inClientData);
private:
CAMutex mMutex { "Device Control Sync" };
bool mActive = false;
CAHALAudioSystemObject mAudioSystem;
BGMAudioDevice mBGMDevice { (AudioObjectID)kAudioObjectUnknown };
BGMAudioDevice mOutputDevice { (AudioObjectID)kAudioObjectUnknown };
BGMDeviceControlsList mBGMDeviceControlsList;
};
#pragma clang assume_nonnull end
#endif /* __BGMApp__BGMDeviceControlSync__ */
#endif /* BGMApp__BGMDeviceControlSync */

View file

@ -0,0 +1,459 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMDeviceControlsList.cpp
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
// Self Include
#include "BGMDeviceControlsList.h"
// Local Includes
#include "BGM_Types.h"
#include "BGM_Utils.h"
// PublicUtility Includes
#include "CAPropertyAddress.h"
#include "CACFArray.h"
#pragma clang assume_nonnull begin
static const SInt64 kToggleDeviceInitialDelay = 50 * NSEC_PER_MSEC;
static const SInt64 kToggleDeviceBackDelay = 500 * NSEC_PER_MSEC;
static const SInt64 kDisableNullDeviceDelay = 500 * NSEC_PER_MSEC;
static const SInt64 kDisableNullDeviceTimeout = 5000 * NSEC_PER_MSEC;
#pragma mark Construction/Destruction
BGMDeviceControlsList::BGMDeviceControlsList(AudioObjectID inBGMDevice,
CAHALAudioSystemObject inAudioSystem)
:
mBGMDevice(inBGMDevice),
mAudioSystem(inAudioSystem),
mDeviceToggleBlock(CreateDeviceToggleBlock()),
mDeviceToggleBackBlock(CreateDeviceToggleBackBlock()),
mDisableNullDeviceBlock(CreateDisableNullDeviceBlock())
{
BGMAssert((mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)) ||
mBGMDevice.GetObjectID() == kAudioObjectUnknown),
"BGMDeviceControlsList::BGMDeviceControlsList: Given device is not BGMDevice");
// Register a listener to find out when the Null Device becomes available/unavailable. See
// ToggleDefaultDevice.
dispatch_queue_attr_t attr =
dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT, 0);
mListenerQueue = dispatch_queue_create("com.bearisdriving.BGM.BGMDeviceControlsList", attr);
mListenerBlock = ^(UInt32 inNumberAddresses, const AudioObjectPropertyAddress* inAddresses) {
// Ignore the notification if we're not toggling the default device, which would just mean
// the default device has been changed for an unrelated reason.
if(mDeviceToggleState == ToggleState::NotToggling)
{
return;
}
for(int i = 0; i < inNumberAddresses; i++)
{
switch(inAddresses[i].mSelector)
{
case kAudioHardwarePropertyDevices:
{
CAMutex::Locker locker(mMutex);
DebugMsg("BGMDeviceControlsList::mListenerBlock: Got "
"kAudioHardwarePropertyDevices");
// Cancel the previous block in case it hasn't run yet.
DestroyBlock(mDeviceToggleBlock);
mDeviceToggleBlock = CreateDeviceToggleBlock();
// Changing the default device too quickly after enabling the Null Device
// seems to cause problems with some programs. Not sure why.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceInitialDelay),
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
mDeviceToggleBlock);
}
break;
}
}
};
BGMLogAndSwallowExceptions("BGMDeviceControlsList::BGMDeviceControlsList", [&] {
mAudioSystem.AddPropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
mListenerQueue,
mListenerBlock);
});
}
BGMDeviceControlsList::~BGMDeviceControlsList()
{
CAMutex::Locker locker(mMutex);
BGMLogAndSwallowExceptions("BGMDeviceControlsList::~BGMDeviceControlsList", [&] {
mAudioSystem.RemovePropertyListenerBlock(CAPropertyAddress(kAudioHardwarePropertyDevices),
mListenerQueue,
mListenerBlock);
});
// If we're in the middle of toggling the default device, block until we've finished.
if(mDisableNullDeviceBlock && mDeviceToggleState != ToggleState::NotToggling)
{
DebugMsg("BGMDeviceControlsList::~BGMDeviceControlsList: Waiting for device toggle");
// Copy the reference so we can unlock the mutex and allow any remaining blocks to run.
dispatch_block_t disableNullDeviceBlock = mDisableNullDeviceBlock;
CAMutex::Unlocker unlocker(mMutex);
// Note that if mDisableNullDeviceBlock is currently running this will return after it
// finishes and if it's already run this will return immediately. So we don't have to
// worry about ending up waiting for mDisableNullDeviceBlock when it isn't queued.
bool timedOut = dispatch_block_wait(disableNullDeviceBlock, kDisableNullDeviceTimeout);
if(timedOut)
{
LogWarning("BGMDeviceControlsList::~BGMDeviceControlsList: Device toggle timed out");
}
}
mDeviceToggleState = ToggleState::NotToggling;
DestroyBlock(mDeviceToggleBlock);
DestroyBlock(mDeviceToggleBackBlock);
DestroyBlock(mDisableNullDeviceBlock);
Block_release(mListenerBlock);
dispatch_release(mListenerQueue);
}
void BGMDeviceControlsList::SetBGMDevice(AudioObjectID inBGMDeviceID)
{
CAMutex::Locker locker(mMutex);
mBGMDevice = inBGMDeviceID;
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
"BGMDeviceControlsList::SetBGMDevice: Given device is not BGMDevice");
}
bool BGMDeviceControlsList::MatchControlsListOf(AudioObjectID inDeviceID)
{
CAMutex::Locker locker(mMutex);
if(mBGMDevice == kAudioObjectUnknown)
{
return false;
}
// If the output device doesn't have a control that BGMDevice does, disable it on BGMDevice so
// the system's audio UI isn't confusing.
// No need to change input controls.
AudioObjectPropertyScope inScope = kAudioObjectPropertyScopeOutput;
// Check which of BGMDevice's controls are currently enabled. We need to know whether we're
// actually enabling/disabling any controls so we know whether we need to call
// PropagateControlListChange afterward.
CFTypeRef __nullable enabledControlsRef =
mBGMDevice.GetPropertyData_CFType(kBGMEnabledOutputControlsAddress);
ThrowIf(!enabledControlsRef || (CFGetTypeID(enabledControlsRef) != CFArrayGetTypeID()),
CAException(kAudioHardwareIllegalOperationError),
"BGMDeviceControlsList::MatchControlsListOf: Expected a CFArray for "
"kAudioDeviceCustomPropertyEnabledOutputControls");
CACFArray enabledControls(static_cast<CFArrayRef>(enabledControlsRef), true);
BGMAssert(enabledControls.GetNumberItems() == 2,
"BGMDeviceControlsList::MatchControlsListOf: Expected 2 array elements for "
"kAudioDeviceCustomPropertyEnabledOutputControls");
bool volumeEnabled;
bool didGetBool = enabledControls.GetBool(kBGMEnabledOutputControlsIndex_Volume, volumeEnabled);
ThrowIf(!didGetBool,
CAException(kAudioHardwareIllegalOperationError),
"BGMDeviceControlsList::MatchControlsListOf: Expected volume element of "
"kAudioDeviceCustomPropertyEnabledOutputControls to be a CFBoolean");
bool muteEnabled;
didGetBool = enabledControls.GetBool(kBGMEnabledOutputControlsIndex_Mute, muteEnabled);
ThrowIf(!didGetBool,
CAException(kAudioHardwareIllegalOperationError),
"BGMDeviceControlsList::MatchControlsListOf: Expected mute element of "
"kAudioDeviceCustomPropertyEnabledOutputControls to be a CFBoolean");
DebugMsg("BGMDeviceControlsList::MatchControlsListOf: BGMDevice has volume %s, mute %s",
(volumeEnabled ? "enabled" : "disabled"),
(muteEnabled ? "enabled" : "disabled"));
// Check which controls the other device has.
BGMAudioDevice device(inDeviceID);
bool hasMute = device.HasSettableMasterMute(inScope);
bool hasVolume =
device.HasSettableMasterVolume(inScope) || device.HasSettableVirtualMasterVolume(inScope);
if(!hasVolume)
{
// Check for per-channel volume controls.
UInt32 numChannels =
device.GetTotalNumberChannels(inScope == kAudioObjectPropertyScopeInput);
for(UInt32 channel = 1; channel <= numChannels; channel++)
{
BGMLogAndSwallowExceptionsMsg("BGMDeviceControlsList::MatchControlsListOf",
"Checking for channel volume controls",
([&] {
hasVolume =
(device.HasVolumeControl(inScope, channel)
&& device.VolumeControlIsSettable(inScope, channel));
}));
if(hasVolume)
{
break;
}
}
}
// Tell BGMDevice to enable/disable its controls to match the output device.
bool deviceUpdated = false;
CACFArray newEnabledControls;
newEnabledControls.SetCFMutableArrayFromCopy(enabledControls.GetCFArray());
// Update volume.
if(volumeEnabled != hasVolume)
{
DebugMsg("BGMDeviceControlsList::MatchControlsListOf: %s BGMDevice volume control.",
hasVolume ? "Enabling" : "Disabling");
newEnabledControls.SetBool(kBGMEnabledOutputControlsIndex_Volume, hasVolume);
deviceUpdated = true;
}
// Update mute.
if(muteEnabled != hasMute)
{
DebugMsg("BGMDeviceControlsList::MatchControlsListOf: %s BGMDevice mute control.",
hasMute ? "Enabling" : "Disabling");
newEnabledControls.SetBool(kBGMEnabledOutputControlsIndex_Mute, hasMute);
deviceUpdated = true;
}
if(deviceUpdated)
{
mBGMDevice.SetPropertyData_CFType(kBGMEnabledOutputControlsAddress,
newEnabledControls.GetCFMutableArray());
}
return deviceUpdated;
}
void BGMDeviceControlsList::PropagateControlListChange()
{
CAMutex::Locker locker(mMutex);
if(mBGMDevice == kAudioObjectUnknown)
{
return;
}
// Leave the default device alone if the user has changed it since launching BGMApp.
bool bgmDeviceIsDefault = true;
BGMLogAndSwallowExceptions("BGMDeviceControlsList::PropagateControlListChange", ([&] {
bgmDeviceIsDefault =
(mBGMDevice.GetObjectID() == mAudioSystem.GetDefaultAudioDevice(false, false));
}));
if(bgmDeviceIsDefault)
{
mDeviceToggleState = ToggleState::SettingNullDeviceAsDefault;
// We'll get a notification from the HAL after the Null Device is enabled. Then we can
// temporarily make it the default device, which gets other programs to notice that
// BGMDevice's controls have changed.
try
{
CAMutex::Unlocker unlocker(mMutex);
SetNullDeviceEnabled(true);
}
catch (...)
{
mDeviceToggleState = ToggleState::NotToggling;
LogError("BGMDeviceControlsList::PropagateControlListChange: Could not enable the Null "
"Device");
throw;
}
}
}
#pragma mark Toggling the Default Device
void BGMDeviceControlsList::ToggleDefaultDevice()
{
// Set the Null Device as the OS X default device.
AudioObjectID nullDeviceID = mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMNullDeviceUID));
if(nullDeviceID == kAudioObjectUnknown)
{
// It's unlikely, but we might have been notified about an unrelated device so just log a
// warning.
LogWarning("BGMDeviceControlsList::ToggleDefaultDevice: Null Device not found");
return;
}
DebugMsg("BGMDeviceControlsList::ToggleDefaultDevice: Setting Null Device as default. "
"nullDeviceID = %u", nullDeviceID);
mAudioSystem.SetDefaultAudioDevice(false, false, nullDeviceID);
mDeviceToggleState = ToggleState::SettingBGMDeviceAsDefault;
// A small number of apps (e.g. Firefox) seem to have trouble with the default device being
// changed back immediately, so for now we insert a short delay here and before disabling the
// Null Device.
// Cancel the previous block in case it hasn't run yet.
DestroyBlock(mDeviceToggleBackBlock);
mDeviceToggleBackBlock = CreateDeviceToggleBackBlock();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kToggleDeviceBackDelay),
dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0),
mDeviceToggleBackBlock);
}
void BGMDeviceControlsList::SetNullDeviceEnabled(bool inEnabled)
{
DebugMsg("BGMDeviceControlsList::SetNullDeviceEnabled: %s the null device",
inEnabled ? "Enabling" : "Disabling");
// Get the audio object for BGMDriver, which is the object the Null Device belongs to.
AudioObjectID bgmDriverID = mAudioSystem.GetAudioPlugInForBundleID(CFSTR(kBGMDriverBundleID));
if(bgmDriverID == kAudioObjectUnknown)
{
LogError("BGMDeviceControlsList::SetNullDeviceEnabled: BGMDriver plug-in audio object not "
"found");
throw CAException(kAudioHardwareUnspecifiedError);
}
CAHALAudioObject bgmDriver(bgmDriverID);
bgmDriver.SetPropertyData_CFType(CAPropertyAddress(kAudioPlugInCustomPropertyNullDeviceActive),
(inEnabled ? kCFBooleanTrue : kCFBooleanFalse));
}
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBlock()
{
return dispatch_block_create((dispatch_block_flags_t)0, ^{
CAMutex::Locker locker(mMutex);
if(mDeviceToggleState == ToggleState::SettingNullDeviceAsDefault)
{
BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDeviceToggleBlock",
([&] {
ToggleDefaultDevice();
}));
}
});
}
dispatch_block_t BGMDeviceControlsList::CreateDeviceToggleBackBlock()
{
return dispatch_block_create((dispatch_block_flags_t)0, ^{
CAMutex::Locker locker(mMutex);
if(mDeviceToggleState != ToggleState::SettingBGMDeviceAsDefault)
{
return;
}
// Set BGMDevice back as the default device.
DebugMsg("BGMDeviceControlsList::ToggleDefaultDevice: Setting BGMDevice as default");
BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDeviceToggleBackBlock", ([&] {
mAudioSystem.SetDefaultAudioDevice(false, false, mBGMDevice.GetObjectID());
}));
mDeviceToggleState = ToggleState::DisablingNullDevice;
// Cancel the previous block in case it hasn't run yet.
DestroyBlock(mDisableNullDeviceBlock);
mDisableNullDeviceBlock = CreateDisableNullDeviceBlock();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kDisableNullDeviceDelay),
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0),
mDisableNullDeviceBlock);
});
}
dispatch_block_t BGMDeviceControlsList::CreateDisableNullDeviceBlock()
{
return dispatch_block_create((dispatch_block_flags_t)0, ^{
CAMutex::Locker locker(mMutex);
if(mDeviceToggleState != ToggleState::DisablingNullDevice)
{
return;
}
mDeviceToggleState = ToggleState::NotToggling;
BGMLogAndSwallowExceptions("BGMDeviceControlsList::CreateDisableNullDeviceBlock",
([&] {
CAMutex::Unlocker unlocker(mMutex);
// Hide the null device from the user again.
SetNullDeviceEnabled(false);
}));
BGMAssert(mBGMDevice.GetObjectID() == mAudioSystem.GetAudioDeviceForUID(CFSTR(kBGMDeviceUID)),
"BGMDevice's AudioObjectID changed");
});
}
void BGMDeviceControlsList::DestroyBlock(dispatch_block_t __nullable & block)
{
if(!block)
{
return;
}
dispatch_block_t& blockNN = (dispatch_block_t&)block;
if(!dispatch_block_testcancel(blockNN))
{
// Stop the block from running if it's currently queued.
dispatch_block_cancel(blockNN);
// Make sure the block isn't currently running. That should almost never be the case.
while(!dispatch_block_testcancel(blockNN))
{
CAMutex::Unlocker unlocker(mMutex);
usleep(10);
}
Block_release(block);
block = nullptr;
}
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,125 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMDeviceControlsList.h
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
#ifndef BGMApp__BGMDeviceControlsList
#define BGMApp__BGMDeviceControlsList
// Local Includes
#include "BGMAudioDevice.h"
// PublicUtility Includes
#include "CAHALAudioDevice.h"
#include "CAHALAudioSystemObject.h"
#include "CAMutex.h"
// System Includes
#include <dispatch/dispatch.h>
#include <AudioToolbox/AudioServices.h>
#pragma clang assume_nonnull begin
class BGMDeviceControlsList
{
#pragma mark Construction/Destruction
public:
BGMDeviceControlsList(AudioObjectID inBGMDevice,
CAHALAudioSystemObject inAudioSystem
= CAHALAudioSystemObject());
~BGMDeviceControlsList();
// Disallow copying
BGMDeviceControlsList(const BGMDeviceControlsList&) = delete;
BGMDeviceControlsList& operator=(const BGMDeviceControlsList&) = delete;
/*! @param inBGMDeviceID The ID of BGMDevice. */
void SetBGMDevice(AudioObjectID inBGMDeviceID);
/*!
Enable the BGMDevice controls (volume and mute currently) that can be matched to controls of
the given device, and disable the ones that can't.
@param inDeviceID The ID of the device.
@return True if BGMDevice's list of controls was updated.
@throws CAException if an error is received from either device.
*/
bool MatchControlsListOf(AudioObjectID inDeviceID);
/*!
After updating BGMDevice's controls list, we need to change the default device so programs
(including OS X's audio UI) will update themselves. We could just change to the real output
device and change back, but that could have side effects the user wouldn't expect. For example,
an app the user has muted might be unmuted for a short period.
Instead we tell BGMDriver to enable the Null Device -- a device that does nothing -- so we can
use it to toggle the default device. The Null Device is normally disabled so it can be hidden
from the user. OS X won't let us make a hidden device temporarily visible or set a hidden
device as the default, so we have to completely remove the Null Device from the system while
we're not using it.
@throws CAException if it fails to enable the Null Device.
*/
void PropagateControlListChange();
private:
/*! Changes the OS X default audio device to the Null Device and then back to BGMDevice. */
void ToggleDefaultDevice();
/*!
Enable or disable the Null Device. See PropagateControlListChange and BGM_NullDevice in
BGMDriver.
@throws CAException if we can't get the BGMDriver plug-in audio object from the HAL or the HAL
returns an error when setting kAudioPlugInCustomPropertyNullDeviceActive.
*/
void SetNullDeviceEnabled(bool inEnabled);
dispatch_block_t CreateDeviceToggleBlock();
dispatch_block_t CreateDeviceToggleBackBlock();
dispatch_block_t CreateDisableNullDeviceBlock();
void DestroyBlock(dispatch_block_t __nullable & block);
private:
CAMutex mMutex { "Device Controls List" };
BGMAudioDevice mBGMDevice;
CAHALAudioSystemObject mAudioSystem; // Not guarded by the mutex.
enum ToggleState
{
NotToggling, SettingNullDeviceAsDefault, SettingBGMDeviceAsDefault, DisablingNullDevice
};
BGMDeviceControlsList::ToggleState mDeviceToggleState = ToggleState::NotToggling;
dispatch_block_t mDeviceToggleBlock;
dispatch_block_t mDeviceToggleBackBlock;
dispatch_block_t mDisableNullDeviceBlock;
dispatch_queue_t mListenerQueue;
AudioObjectPropertyListenerBlock mListenerBlock;
};
#pragma clang assume_nonnull end
#endif /* BGMApp__BGMDeviceControlsList */

View file

@ -30,6 +30,7 @@
// PublicUtility Includes
#include "CAAtomic.h"
#include "CAHALAudioSystemObject.h"
#include "CAPropertyAddress.h"
// STL Includes
#include <algorithm> // For std::max
@ -40,20 +41,12 @@
#include <mach/task.h>
// The number of IO cycles (roughly) to wait for our IOProcs to stop themselves before assuming something
// went wrong. If that happens, we try to stop them from a non-IO thread and continue anyway.
static const UInt32 kStopIOProcTimeoutInIOCycles = 600;
#pragma mark Construction/Destruction
static const AudioObjectPropertyAddress kDeviceIsRunningAddress = {
kAudioDevicePropertyDeviceIsRunning,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMaster
};
static const AudioObjectPropertyAddress kProcessorOverloadAddress = {
kAudioDeviceProcessorOverload,
kAudioObjectPropertyScopeGlobal,
kAudioObjectPropertyElementMaster
};
BGMPlayThrough::BGMPlayThrough(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice)
:
mInputDevice(inInputDevice),
@ -149,10 +142,10 @@ void BGMPlayThrough::Activate()
DebugMsg("BGMPlayThrough::Activate: Registering for notifications from BGMDevice.");
mInputDevice.AddPropertyListener(kDeviceIsRunningAddress,
mInputDevice.AddPropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning),
&BGMPlayThrough::BGMDeviceListenerProc,
this);
mInputDevice.AddPropertyListener(kProcessorOverloadAddress,
mInputDevice.AddPropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload),
&BGMPlayThrough::BGMDeviceListenerProc,
this);
@ -196,13 +189,13 @@ void BGMPlayThrough::Deactivate()
// There's not much we can do if these calls throw. The docs for AudioObjectRemovePropertyListener
// just say that means it failed.
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
mInputDevice.RemovePropertyListener(kDeviceIsRunningAddress,
mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning),
&BGMPlayThrough::BGMDeviceListenerProc,
this);
});
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
mInputDevice.RemovePropertyListener(kProcessorOverloadAddress,
mInputDevice.RemovePropertyListener(CAPropertyAddress(kAudioDeviceProcessorOverload),
&BGMPlayThrough::BGMDeviceListenerProc,
this);
});
@ -213,8 +206,10 @@ void BGMPlayThrough::Deactivate()
this);
});
}
Stop();
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
Stop();
});
BGMLogAndSwallowExceptions("BGMPlayThrough::Deactivate", [&]() {
DestroyIOProcIDs();
@ -663,8 +658,8 @@ OSStatus BGMPlayThrough::Stop()
mOutputDeviceIOProcState = IOState::Stopping;
}
// Wait for the IOProcs to stop themselves, with a timeout of about four IO cycles. This is so the IOProcs don't get
// called after the BGMPlayThrough instance (pointed to by the client data they get from the HAL) is deallocated.
// Wait for the IOProcs to stop themselves. This is so the IOProcs don't get called after the BGMPlayThrough instance
// (pointed to by the client data they get from the HAL) is deallocated.
//
// From Jeff Moore on the Core Audio mailing list:
// Note that there is no guarantee about how many times your IOProc might get called after AudioDeviceStop() returns
@ -680,7 +675,7 @@ OSStatus BGMPlayThrough::Stop()
static_cast<UInt64>(std::max(expectedInputCycleNs, expectedOutputCycleNs));
while((mInputDeviceIOProcState == IOState::Stopping || mOutputDeviceIOProcState == IOState::Stopping)
&& (totalWaitNs < 4 * expectedMaxCycleNs))
&& (totalWaitNs < kStopIOProcTimeoutInIOCycles * expectedMaxCycleNs))
{
// TODO: If playthrough is started again while we're waiting in this loop we could drop frames. Wait on a
// semaphore instead of sleeping? That way Start() could also signal it, before waiting on the state mutex,
@ -694,25 +689,25 @@ OSStatus BGMPlayThrough::Stop()
// Clean up if the IOProcs didn't stop themselves
if(mInputDeviceIOProcState == IOState::Stopping && mInputDeviceIOProcID != nullptr)
{
LogWarning("BGMPlayThrough::Stop: The input IOProc didn't stop itself in time. Stopping "
"it from outside of the IO thread.");
LogError("BGMPlayThrough::Stop: The input IOProc didn't stop itself in time. Stopping "
"it from outside of the IO thread.");
BGMLogUnexpectedExceptions("BGMPlayThrough::Stop", [&]() {
mInputDevice.StopIOProc(mInputDeviceIOProcID);
});
mInputDeviceIOProcState = IOState::Stopped;
}
if(mOutputDeviceIOProcState == IOState::Stopping && mOutputDeviceIOProcID != nullptr)
{
LogWarning("BGMPlayThrough::Stop: The output IOProc didn't stop itself in time. Stopping "
"it from outside of the IO thread.");
LogError("BGMPlayThrough::Stop: The output IOProc didn't stop itself in time. Stopping "
"it from outside of the IO thread.");
BGMLogUnexpectedExceptions("BGMPlayThrough::Stop", [&]() {
mOutputDevice.StopIOProc(mOutputDeviceIOProcID);
});
mOutputDeviceIOProcState = IOState::Stopped;
}

View file

@ -77,12 +77,14 @@ public:
private:
/*! @throws CAException */
void Init(CAHALAudioDevice inInputDevice, CAHALAudioDevice inOutputDevice);
public:
/*! @throws CAException */
void Activate();
/*! @throws CAException */
void Deactivate();
private:
void AllocateBuffer();
static bool IsBGMDevice(CAHALAudioDevice inDevice);
@ -170,8 +172,8 @@ private:
AudioDeviceIOProcID __nullable mInputDeviceIOProcID;
AudioDeviceIOProcID __nullable mOutputDeviceIOProcID;
CAHALAudioDevice mInputDevice { kAudioDeviceUnknown };
CAHALAudioDevice mOutputDevice { kAudioDeviceUnknown };
CAHALAudioDevice mInputDevice { kAudioObjectUnknown };
CAHALAudioDevice mOutputDevice { kAudioObjectUnknown };
CAMutex mStateMutex { "Playthrough state" };

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" systemVersion="16D32" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<development version="7000" identifier="xcode"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="12120"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
@ -13,7 +13,7 @@
</customObject>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate">
<customObject id="Voe-Tx-rLC" customClass="BGMAppDelegate">
<connections>
<outlet property="aboutPanel" destination="Cf4-3V-gl1" id="cgo-Hw-rE2"/>
<outlet property="aboutPanelLicenseView" destination="LSG-PF-cl8" id="mbu-kv-Jfc"/>
@ -59,13 +59,14 @@
</connections>
</menuItem>
</items>
<accessibility description="Background Music Main Menu" identifier="MainMenu"/>
<point key="canvasLocation" x="-184" y="-69.5"/>
</menu>
<customView wantsLayer="YES" id="MWB-XH-kFI">
<rect key="frame" x="0.0" y="0.0" width="269" height="47"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField identifier="AppName" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Xmd-bg-huG" customClass="BGMAVM_AppNameLabel">
<textField identifier="AppName" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Xmd-bg-huG" customClass="BGMAVM_AppNameLabel">
<rect key="frame" x="42" y="28" width="115" height="14"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="truncatingTail" allowsUndo="NO" sendsActionOnEndEditing="YES" alignment="left" title="App name here" usesSingleLineMode="YES" id="ZHF-ZW-Oqg">
@ -102,7 +103,7 @@
</configuration>
</ciFilter>
</contentFilters>
<buttonCell key="cell" type="square" title="⌃" bezelStyle="shadowlessSquare" image="747DB04F-A1B8-4533-9C4F-E9A8593F7F7F" alignment="center" lineBreakMode="truncatingTail" imageScaling="proportionallyDown" inset="2" id="IXo-C7-3uE">
<buttonCell key="cell" type="square" title="⌃" bezelStyle="shadowlessSquare" image="buttonCell:IXo-C7-3uE:image" alignment="center" lineBreakMode="truncatingTail" imageScaling="proportionallyDown" inset="2" id="IXo-C7-3uE">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
@ -139,7 +140,7 @@
<rect key="frame" x="0.0" y="0.0" width="1002" height="335"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r51-dd-LGP">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r51-dd-LGP">
<rect key="frame" x="71" y="125" width="240" height="22"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Background Music" id="Dw2-nu-eBQ">
@ -148,7 +149,7 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="1" translatesAutoresizingMaskIntoConstraints="NO" id="ekc-h0-I43">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="1" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ekc-h0-I43">
<rect key="frame" x="71" y="100" width="240" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Version 0.1.0" id="FDH-7l-wFf">
@ -157,7 +158,7 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L5P-Lw-aCd">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="L5P-Lw-aCd">
<rect key="frame" x="413" y="298" width="270" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="left" title="Licensed under GPL v2 or any later version." id="ETh-En-bzX">
@ -170,7 +171,7 @@
<rect key="frame" x="383" y="93" width="5" height="150"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</box>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" translatesAutoresizingMaskIntoConstraints="NO" id="Vy4-dv-jQB">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="2" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Vy4-dv-jQB">
<rect key="frame" x="18" y="75" width="346" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" title="Copyright © 2016, 2017 Background Music contributors" placeholderString="" id="ctF-95-uVu">
@ -179,7 +180,7 @@
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="3" translatesAutoresizingMaskIntoConstraints="NO" id="nx6-kQ-N8Z" customClass="BGMLinkField">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" tag="3" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nx6-kQ-N8Z" customClass="BGMLinkField">
<rect key="frame" x="36" y="50" width="310" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="center" tag="3" title="https://github.com/kyleneideck/BackgroundMusic" placeholderString="" id="VOb-5X-o3R">
@ -209,7 +210,7 @@
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<clipView key="contentView" ambiguous="YES" id="Cdb-RA-YK0">
<rect key="frame" x="1" y="1" width="565" height="243"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textView ambiguous="YES" editable="NO" importsGraphics="NO" richText="NO" id="LSG-PF-cl8">
<rect key="frame" x="-4" y="0.0" width="572" height="243"/>
@ -230,11 +231,11 @@
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" verticalHuggingPriority="750" horizontal="NO" id="qCC-lY-zQ6">
<rect key="frame" x="550" y="1" width="16" height="243"/>
<rect key="frame" x="-15" y="1" width="16" height="0.0"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6qu-yI-r00">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" allowsCharacterPickerTouchBarItem="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6qu-yI-r00">
<rect key="frame" x="413" y="20" width="203" height="11"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" title="The AirPlay Logo is a trademark of Apple Inc." id="lx7-k3-q16">
@ -247,7 +248,7 @@
</view>
<point key="canvasLocation" x="-200" y="232.5"/>
</window>
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" id="IoN-sN-cCx">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" setsMaxLayoutWidthAtFirstLayout="YES" allowsCharacterPickerTouchBarItem="YES" id="IoN-sN-cCx">
<rect key="frame" x="0.0" y="0.0" width="471" height="180"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="mini" sendsActionOnEndEditing="YES" drawsBackground="YES" id="Ay8-8n-FHi">
@ -261,7 +262,8 @@
</textField>
</objects>
<resources>
<image name="747DB04F-A1B8-4533-9C4F-E9A8593F7F7F" width="1" height="1">
<image name="FermataIcon" width="284" height="284"/>
<image name="buttonCell:IXo-C7-3uE:image" width="1" height="1">
<mutableData key="keyedArchiveRepresentation">
YnBsaXN0MDDUAQIDBAUGPT5YJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK4HCBMU
GR4fIyQrLjE3OlUkbnVsbNUJCgsMDQ4PEBESVk5TU2l6ZVYkY2xhc3NcTlNJbWFnZUZsYWdzVk5TUmVw
@ -312,6 +314,5 @@ ALAAswC1ALcAuQC7AMAA1wDZANsJiwmQCZsJpAm3CbsJxgnPCdQJ3AnfCeQJ8wn3Cf4KBgoTChgKGgoc
CiEKKQosCjEKOQo8Ck4KUQpWAAAAAAAAAgEAAAAAAAAAQQAAAAAAAAAAAAAAAAAAClg
</mutableData>
</image>
<image name="FermataIcon" width="284" height="284"/>
</resources>
</document>

View file

@ -34,5 +34,13 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>BGMApp.sdef</string>
<key>NSServices</key>
<array>
<dict/>
</array>
</dict>
</plist>

View file

@ -25,6 +25,8 @@
// Local Includes
#import "BGM_Utils.h"
#import "BGM_Types.h"
#import "BGMAudioDevice.h"
// PublicUtility Includes
#include "CAHALAudioSystemObject.h"
@ -67,29 +69,26 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
audioSystem.GetAudioDevices(numDevices, devices);
for (UInt32 i = 0; i < numDevices; i++) {
CAHALAudioDevice device(devices[i]);
[self insertMenuItemsForDevice:device preferencesMenu:prefsMenu];
[self insertMenuItemsForDevice:devices[i] preferencesMenu:prefsMenu];
}
}
}
- (void) insertMenuItemsForDevice:(CAHALAudioDevice)device preferencesMenu:(NSMenu*)prefsMenu {
- (void) insertMenuItemsForDevice:(BGMAudioDevice)device preferencesMenu:(NSMenu*)prefsMenu {
// Insert menu items after the item for the "Output Device" heading.
const NSInteger menuItemsIdx = [prefsMenu indexOfItemWithTag:kOutputDeviceMenuItemTag] + 1;
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", [&]() {
BOOL isBGMDevice = (device.GetObjectID() == [audioDevices bgmDevice].GetObjectID());
BOOL isHidden = device.IsHidden();
BOOL hasOutputChannels = device.GetTotalNumberChannels(/* inIsInput = */ false) > 0;
BOOL canBeDefault = device.CanBeDefaultDevice(/* inIsInput = */ false, /* inIsSystem = */ false);
if (!isBGMDevice && !isHidden && hasOutputChannels && canBeDefault) {
for (NSMenuItem* item : [self createMenuItemsForDevice:device]) {
[prefsMenu insertItem:item atIndex:menuItemsIdx];
[outputDeviceMenuItems addObject:item];
}
BOOL canBeOutputDevice = YES;
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::insertMenuItemsForDevice", ([&] {
canBeOutputDevice = device.CanBeOutputDeviceInBGMApp();
}));
if (canBeOutputDevice) {
for (NSMenuItem* item : [self createMenuItemsForDevice:device]) {
[prefsMenu insertItem:item atIndex:menuItemsIdx];
[outputDeviceMenuItems addObject:item];
}
});
}
}
- (NSArray<NSMenuItem*>*) createMenuItemsForDevice:(CAHALAudioDevice)device {
@ -108,7 +107,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
// TODO: Use the current data source's name when the control isn't settable, but only add one menu item.
UInt32 numDataSources = 0;
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&]() {
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&] {
if (device.HasDataSourceControl(scope, channel) &&
device.DataSourceControlIsSettable(scope, channel)) {
numDataSources = device.GetNumberAvailableDataSources(scope, channel);
@ -141,12 +140,12 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
DebugMsg("BGMOutputDevicePrefs::createMenuItemsForDevice: Creating item. %s%u",
"Device ID:", device.GetObjectID());
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", [&]() {
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemsForDevice", ([&] {
[items addObject:[self createMenuItemForDevice:device
dataSourceID:nil
title:CFBridgingRelease(device.CopyName())
toolTip:nil]];
});
}));
}
return items;
@ -168,7 +167,7 @@ static NSInteger const kOutputDeviceMenuItemTag = 2;
// Add the AirPlay icon to the labels of AirPlay devices.
//
// TODO: Test this with real hardware that supports AirPlay. (I don't have any.)
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemForDevice", [&]() {
BGMLogAndSwallowExceptions("BGMOutputDevicePrefs::createMenuItemForDevice", [&] {
if (device.GetTransportType() == kAudioDeviceTransportTypeAirPlay) {
item.image = [NSImage imageNamed:@"AirPlayIcon"];

View file

@ -0,0 +1,46 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMASOutputDevice.h
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
// An AppleScript class for the output devices that can be selected in the preferences menu.
//
// Local Includes
#import "BGMAudioDeviceManager.h"
// System Includes
#import <Foundation/Foundation.h>
#pragma clang assume_nonnull begin
@interface BGMASOutputDevice : NSObject
- (instancetype) initWithAudioObjectID:(AudioObjectID)objID
audioDevices:(BGMAudioDeviceManager*)devices
parentSpecifier:(NSScriptObjectSpecifier* __nullable)parentSpecifier;
@property (readonly) NSString* name;
@property BOOL selected; // is this the device to be used for audio output?
@end
#pragma clang assume_nonnull end

View file

@ -0,0 +1,83 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMASOutputDevice.mm
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
// Self Include
#import "BGMASOutputDevice.h"
// Local Includes
#import "BGMAudioDevice.h"
// PublicUtility Includes
#import "CADebugMacros.h"
#pragma clang assume_nonnull begin
@implementation BGMASOutputDevice {
NSScriptObjectSpecifier* parentSpecifier;
BGMAudioDevice device;
BGMAudioDeviceManager* audioDevices;
}
- (instancetype) initWithAudioObjectID:(AudioObjectID)objID
audioDevices:(BGMAudioDeviceManager*)devices
parentSpecifier:(NSScriptObjectSpecifier* __nullable)parent {
if ((self = [super init])) {
parentSpecifier = parent;
device = objID;
audioDevices = devices;
}
return self;
}
- (NSString*) name {
return (NSString*)CFBridgingRelease(device.CopyName());
}
- (BOOL) selected {
return [audioDevices isOutputDevice:device];
}
- (void) setSelected:(BOOL)selected {
if (selected && ![self selected]) {
DebugMsg("BGMASOutputDevice::setSelected: A script is setting output device to %s",
[[self name] UTF8String]);
NSError* err = [audioDevices setOutputDeviceWithID:device revertOnFailure:YES];
(void)err; // TODO: Return an error to the script somehow if this isn't nil. Also, should
// we return an error if the script tries to set this property to false?
}
}
- (NSScriptObjectSpecifier* __nullable) objectSpecifier {
NSScriptClassDescription* parentClassDescription = [parentSpecifier keyClassDescription];
return [[NSNameSpecifier alloc] initWithContainerClassDescription:parentClassDescription
containerSpecifier:parentSpecifier
key:@"output devices"
name:self.name];
}
@end
#pragma clang assume_nonnull end

View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary>
<suite name="Background Music" code="BGMs" description="Background Music specific classes">
<!-- TODO: Figure out why Script Editor returns output device objects as
«class» "Built-in Output" of application "Background Music"
instead of
device "Built-in Output" of application "Background Music" -->
<class name="output device"
code="aDev"
description="A hardware device that can play audio"
plural="output devices"
inherits="item">
<synonym name="audio device"/>
<cocoa class="BGMASOutputDevice"/>
<property name="name"
code="pnam"
description="The name of the output device."
type="text"
access="r"/>
<property name="selected"
code="Slcd"
type="boolean"
access="rw"
description="Is this the device to be used for audio output?">
<synonym name="default"/>
</property>
</class>
<class name="application"
code="capp"
description="The application program">
<cocoa class="NSApplication"/>
<!-- We have a class called "output device", so we have to call this class something
else. -->
<property name="selected output device"
type="output device"
code="selO"
access="rw"
description="The device to be used for audio output">
<synonym name="selected device"/>
<synonym name="default device"/>
<synonym name="default output device"/>
<cocoa key="selectedOutputDevice"/>
</property>
<!-- Unintuitively, this is for the array of output devices. -->
<element type="output device" access="r">
<cocoa key="outputDevices"/>
</element>
</class>
</suite>
</dictionary>

View file

@ -0,0 +1,44 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMAppDelegate+AppleScript.h
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
#import "BGMAppDelegate.h"
// Local Includes
#import "BGMASOutputDevice.h"
// System Includes
#import <Foundation/Foundation.h>
#pragma clang assume_nonnull begin
@interface BGMAppDelegate (AppleScript)
- (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key;
@property BGMASOutputDevice* selectedOutputDevice;
@property (readonly) NSArray<BGMASOutputDevice*>* outputDevices;
@end
#pragma clang assume_nonnull end

View file

@ -0,0 +1,85 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGMAppDelegate+AppleScript.mm
// BGMApp
//
// Copyright © 2017 Kyle Neideck
//
// Self Include
#import "BGMAppDelegate+AppleScript.h"
// Local Includes
#import "BGMAudioDevice.h"
// PublicUtility Includes
#import "CAHALAudioSystemObject.h"
#import "CAAutoDisposer.h"
#pragma clang assume_nonnull begin
@implementation BGMAppDelegate (AppleScript)
- (BOOL) application:(NSApplication*)sender delegateHandlesKey:(NSString*)key {
#pragma unused (sender)
DebugMsg("BGMAppDelegate:application:delegateHandlesKey: Key queried: '%s'", [key UTF8String]);
return [@[@"selectedOutputDevice", @"outputDevices"] containsObject:key];
}
- (BGMASOutputDevice*) selectedOutputDevice {
AudioObjectID outputDeviceID = [self.audioDevices outputDevice].GetObjectID();
return [[BGMASOutputDevice alloc] initWithAudioObjectID:outputDeviceID
audioDevices:self.audioDevices
parentSpecifier:[self objectSpecifier]];
}
- (void) setSelectedOutputDevice:(BGMASOutputDevice*)device {
[device setSelected:YES];
}
- (NSArray<BGMASOutputDevice*>*) outputDevices {
UInt32 numDevices = CAHALAudioSystemObject().GetNumberAudioDevices();
CAAutoArrayDelete<AudioObjectID> devices(numDevices);
CAHALAudioSystemObject().GetAudioDevices(numDevices, devices);
NSMutableArray<BGMASOutputDevice*>* outputDevices =
[NSMutableArray arrayWithCapacity:numDevices];
for (UInt32 i = 0; i < numDevices; i++) {
BGMAudioDevice device(devices[i]);
if (device.CanBeOutputDeviceInBGMApp()) {
BGMASOutputDevice* outputDevice =
[[BGMASOutputDevice alloc] initWithAudioObjectID:device.GetObjectID()
audioDevices:self.audioDevices
parentSpecifier:[self objectSpecifier]];
[outputDevices addObject:outputDevice];
}
}
return outputDevices;
}
@end
#pragma clang assume_nonnull end

View file

@ -0,0 +1,31 @@
/*
* BGMApp.h
*/
#import <AppKit/AppKit.h>
#import <ScriptingBridge/ScriptingBridge.h>
@class BGMAppOutputDevice, BGMAppApplication;
/*
* Background Music
*/
// an output audio device
@interface BGMAppOutputDevice : SBObject
@property (copy, readonly) NSString *name;
@property BOOL selected; // is this the device to be used for audio output?
@end
// The application program
@interface BGMAppApplication : SBApplication
- (SBElementArray<BGMAppOutputDevice *> *) outputDevices;
@end

View file

@ -19,11 +19,15 @@
//
// Copyright © 2017 Kyle Neideck
//
// You'll probably want to use Xcode's UI test recording feature if you add new tests.
// You might want to use Xcode's UI test recording feature if you add new tests.
//
// Local includes
// Local Includes
#import "BGM_TestUtils.h"
#import "BGM_Types.h"
// Scripting Bridge Includes
#import "BGMApp.h"
// TODO: Skip these tests if macOS SDK 10.11 or higher isn't available.
@ -58,6 +62,8 @@
icon = [app.menuBars childrenMatchingType:XCUIElementTypeMenuBarItem].element;
prefs = menuItems[@"Preferences"];
// TODO: Make sure BGMDevice isn't set as the OS X default device before launching BGMApp.
// Tell BGMApp not to load/store user defaults (settings) and to use
// NSApplicationActivationPolicyRegular. If it used the "accessory" policy as usual, the tests
// would fail to start because of a bug in Xcode.
@ -81,6 +87,69 @@
[super tearDown];
}
- (void) testCycleOutputDevices {
const int NUM_CYCLES = 2;
// Get the list of output devices from the preferences menu.
[icon click];
[prefs hover];
NSArray<XCUIElement*>* outputDeviceMenuItems = [self outputDeviceMenuItems];
// For debugging certain issues, it can be useful to repeatedly switch between two
// devices:
// outputDeviceMenuItems = [outputDeviceMenuItems subarrayWithRange:NSMakeRange(0,2)];
XCTAssertGreaterThan(outputDeviceMenuItems.count, 0);
// Click the last device to close the menu again.
[outputDeviceMenuItems.lastObject click];
BGMAppApplication* sbApp = [SBApplication applicationWithBundleIdentifier:@kBGMAppBundleID];
for (int i = 0; i < NUM_CYCLES; i++) {
// Select each output device.
for (XCUIElement* item in outputDeviceMenuItems) {
[icon click];
[prefs hover];
[item click];
// Assert that the device we clicked is the selected device now.
for (BGMAppOutputDevice* device in [sbApp outputDevices]) {
// TODO: This seems a bit fragile. Would it still work with long device names?
if ([device.name isEqualToString:[item title]]) {
XCTAssert(device.selected);
} else {
XCTAssertFalse(device.selected);
}
}
}
}
}
// Find the menu items for the output devices in the preferences menu.
- (NSArray<XCUIElement*>*) outputDeviceMenuItems {
NSArray<XCUIElement*>* items = @[];
BOOL inOutputDeviceSection = NO;
for (int i = 0; i < prefs.menuItems.count; i++) {
XCUIElement* menuItem = [prefs.menuItems elementBoundByIndex:i];
if ([menuItem.title isEqual:@"Output Device"]) {
inOutputDeviceSection = YES;
} else if (inOutputDeviceSection) {
// Assume that finding a separator menu item means we've reached the end of the section.
if (((NSMenuItem*)menuItem.value).separatorItem || [menuItem.title isEqual:@""]) {
break;
}
items = [items arrayByAddingObject:menuItem];
}
}
return items;
}
- (void) testSelectMusicPlayer {
// Select VLC as the music player.
[icon click];

View file

@ -14,6 +14,7 @@
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */; };
1C3DB4871BE063C500EC8160 /* BGM_DeviceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C3DB4861BE063C500EC8160 /* BGM_DeviceTests.mm */; };
1C8034DD1BDD073B00668E00 /* BGM_ClientsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */; };
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */; };
1CB8B36E1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */; };
1CB8B3761BBBD924000E2DD1 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3741BBBD924000E2DD1 /* CoreAudio.framework */; };
1CB8B3771BBBD924000E2DD1 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1CB8B3751BBBD924000E2DD1 /* CoreFoundation.framework */; };
@ -27,6 +28,11 @@
1CC1DF931BE7B79500FB8FE4 /* CADebugger.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CC1DF871BE558B000FB8FE4 /* CADebugger.cpp */; };
1CC1DF941BE7B79500FB8FE4 /* CAVolumeCurve.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B38C1BBCF4A9000E2DD1 /* CAVolumeCurve.cpp */; };
1CC1DF9E1BE94AA200FB8FE4 /* DeviceIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 1CC1DF9D1BE94AA200FB8FE4 /* DeviceIcon.icns */; };
1CD95B121E93AA5200EB8EF0 /* BGM_AbstractDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */; };
1CD95B131E93AA5200EB8EF0 /* BGM_NullDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */; };
1CD95B141E93AA5200EB8EF0 /* BGM_Stream.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */; };
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */; };
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */; };
27379B821C76D62D0084A24C /* CADebugMacros.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3701BBBD8A4000E2DD1 /* CADebugMacros.cpp */; };
27379B831C76D62D0084A24C /* CADebugPrintf.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1CB8B3781BBBDFA2000E2DD1 /* CADebugPrintf.cpp */; };
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 27381A141C8EF50F00DF167C /* BGM_XPCHelper.m */; };
@ -77,6 +83,7 @@
1C0CB6B81C642C600084C15A /* BGM_ClientTasks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_ClientTasks.h; sourceTree = "<group>"; };
1C305D9B1BE294B5004EBB91 /* CACFNumber.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFNumber.cpp; path = PublicUtility/CACFNumber.cpp; sourceTree = "<group>"; };
1C305D9C1BE294B5004EBB91 /* CACFNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFNumber.h; path = PublicUtility/CACFNumber.h; sourceTree = "<group>"; };
1C37B3681E9B8D3C000DF98F /* CAPropertyAddress.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CAPropertyAddress.h; path = PublicUtility/CAPropertyAddress.h; sourceTree = "<group>"; };
1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_TaskQueue.cpp; sourceTree = "<group>"; };
1C38210D1C4A163A00A0C8C6 /* BGM_TaskQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_TaskQueue.h; sourceTree = "<group>"; };
1C38210F1C4A18DE00A0C8C6 /* CAPThread.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CAPThread.cpp; path = PublicUtility/CAPThread.cpp; sourceTree = "<group>"; };
@ -85,6 +92,8 @@
1C8034DA1BDD073B00668E00 /* BGMDriverTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BGMDriverTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
1C8034DC1BDD073B00668E00 /* BGM_ClientsTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = BGM_ClientsTests.mm; sourceTree = "<group>"; };
1C8034DE1BDD073B00668E00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_Stream.cpp; sourceTree = "<group>"; };
1CA2A9E11E8D1D08007A76A4 /* BGM_Stream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_Stream.h; sourceTree = "<group>"; };
1CB8B3641BBBB78D000E2DD1 /* Background Music Device.driver */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Background Music Device.driver"; sourceTree = BUILT_PRODUCTS_DIR; };
1CB8B3681BBBB78D000E2DD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
1CB8B36D1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_PlugInInterface.cpp; sourceTree = "<group>"; };
@ -117,6 +126,10 @@
1CC1DF881BE558B000FB8FE4 /* CADebugger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CADebugger.h; path = PublicUtility/CADebugger.h; sourceTree = "<group>"; };
1CC1DF991BE865C000FB8FE4 /* quick_install.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = quick_install.sh; sourceTree = "<group>"; };
1CC1DF9D1BE94AA200FB8FE4 /* DeviceIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = DeviceIcon.icns; sourceTree = "<group>"; };
1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_NullDevice.cpp; sourceTree = "<group>"; };
1CDF3ABB1E863B980001E9B7 /* BGM_NullDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_NullDevice.h; sourceTree = "<group>"; };
1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = BGM_AbstractDevice.cpp; sourceTree = "<group>"; };
1CDF3ABE1E8644C20001E9B7 /* BGM_AbstractDevice.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BGM_AbstractDevice.h; sourceTree = "<group>"; };
1CE3E68C1BE263CA00167F5D /* CACFDictionary.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFDictionary.cpp; path = PublicUtility/CACFDictionary.cpp; sourceTree = "<group>"; };
1CE3E68D1BE263CA00167F5D /* CACFDictionary.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CACFDictionary.h; path = PublicUtility/CACFDictionary.h; sourceTree = "<group>"; };
1CE3E68F1BE2683900167F5D /* CACFArray.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = CACFArray.cpp; path = PublicUtility/CACFArray.cpp; sourceTree = "<group>"; };
@ -218,8 +231,14 @@
1CB8B37B1BBCCF62000E2DD1 /* BGM_PlugIn.cpp */,
1CB8B3821BBCE7B5000E2DD1 /* BGM_Object.h */,
1CB8B3811BBCE7B5000E2DD1 /* BGM_Object.cpp */,
1CDF3ABE1E8644C20001E9B7 /* BGM_AbstractDevice.h */,
1CDF3ABD1E8644C20001E9B7 /* BGM_AbstractDevice.cpp */,
1CB8B37F1BBCCF87000E2DD1 /* BGM_Device.h */,
1CB8B37E1BBCCF87000E2DD1 /* BGM_Device.cpp */,
1CDF3ABB1E863B980001E9B7 /* BGM_NullDevice.h */,
1CDF3ABA1E863B980001E9B7 /* BGM_NullDevice.cpp */,
1CA2A9E11E8D1D08007A76A4 /* BGM_Stream.h */,
1CA2A9E01E8D1D08007A76A4 /* BGM_Stream.cpp */,
1C0CB6AF1C642C600084C15A /* DeviceClients */,
1C38210D1C4A163A00A0C8C6 /* BGM_TaskQueue.h */,
1C38210C1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp */,
@ -246,6 +265,7 @@
isa = PBXGroup;
children = (
1C0CB6A71C4E06F70084C15A /* CAAtomic.h */,
1C37B3681E9B8D3C000DF98F /* CAPropertyAddress.h */,
1C0CB6A61C4E06C00084C15A /* CAAtomicStack.h */,
1C0CB6A91C50A3AF0084C15A /* CAAutoDisposer.h */,
1CE3E68F1BE2683900167F5D /* CACFArray.cpp */,
@ -427,6 +447,9 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1CD95B121E93AA5200EB8EF0 /* BGM_AbstractDevice.cpp in Sources */,
1CD95B131E93AA5200EB8EF0 /* BGM_NullDevice.cpp in Sources */,
1CD95B141E93AA5200EB8EF0 /* BGM_Stream.cpp in Sources */,
27E6B5F01E01966A00EC0AAB /* BGM_Utils.cpp in Sources */,
277170101CA0CFC300AB34B4 /* BGM_PlugInInterface.cpp in Sources */,
277170111CA0CFC300AB34B4 /* CACFNumber.cpp in Sources */,
@ -460,17 +483,20 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
1CA2A9E21E8D1D08007A76A4 /* BGM_Stream.cpp in Sources */,
1CB8B3801BBCCF87000E2DD1 /* BGM_Device.cpp in Sources */,
1C0CB6B91C642C600084C15A /* BGM_Client.cpp in Sources */,
1CB8B3921BBCF50A000E2DD1 /* BGM_WrappedAudioEngine.cpp in Sources */,
1CB8B36E1BBBD541000E2DD1 /* BGM_PlugInInterface.cpp in Sources */,
1CB8B37D1BBCCF62000E2DD1 /* BGM_PlugIn.cpp in Sources */,
27381A161C8EF50F00DF167C /* BGM_XPCHelper.m in Sources */,
1CDF3ABF1E8644C20001E9B7 /* BGM_AbstractDevice.cpp in Sources */,
1C0CB6BA1C642C600084C15A /* BGM_ClientMap.cpp in Sources */,
1CB8B3831BBCE7B5000E2DD1 /* BGM_Object.cpp in Sources */,
275343BD1DE9B44900DF3858 /* BGM_Utils.cpp in Sources */,
1C38210E1C4A163A00A0C8C6 /* BGM_TaskQueue.cpp in Sources */,
1C0CB6BB1C642C600084C15A /* BGM_Clients.cpp in Sources */,
1CDF3ABC1E863B980001E9B7 /* BGM_NullDevice.cpp in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View file

@ -0,0 +1,409 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGM_AbstractDevice.cpp
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Self Include
#include "BGM_AbstractDevice.h"
// Local Includes
#include "BGM_Utils.h"
// PublicUtility Includes
#include "CAException.h"
#include "CADebugMacros.h"
#pragma clang assume_nonnull begin
BGM_AbstractDevice::BGM_AbstractDevice(AudioObjectID inObjectID, AudioObjectID inOwnerObjectID)
:
BGM_Object(inObjectID, kAudioDeviceClassID, kAudioObjectClassID, inOwnerObjectID)
{
}
BGM_AbstractDevice::~BGM_AbstractDevice()
{
}
#pragma mark Property Operations
bool BGM_AbstractDevice::HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioObjectPropertyName:
case kAudioObjectPropertyManufacturer:
case kAudioDevicePropertyDeviceUID:
case kAudioDevicePropertyModelUID:
case kAudioDevicePropertyTransportType:
case kAudioDevicePropertyRelatedDevices:
case kAudioDevicePropertyClockDomain:
case kAudioDevicePropertyDeviceIsAlive:
case kAudioDevicePropertyDeviceIsRunning:
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
case kAudioDevicePropertyLatency:
case kAudioDevicePropertyStreams:
case kAudioObjectPropertyControlList:
case kAudioDevicePropertySafetyOffset:
case kAudioDevicePropertyNominalSampleRate:
case kAudioDevicePropertyAvailableNominalSampleRates:
case kAudioDevicePropertyIsHidden:
case kAudioDevicePropertyZeroTimeStampPeriod:
theAnswer = true;
break;
default:
theAnswer = BGM_Object::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool BGM_AbstractDevice::IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioObjectPropertyName:
case kAudioObjectPropertyManufacturer:
case kAudioDevicePropertyDeviceUID:
case kAudioDevicePropertyModelUID:
case kAudioDevicePropertyTransportType:
case kAudioDevicePropertyRelatedDevices:
case kAudioDevicePropertyClockDomain:
case kAudioDevicePropertyDeviceIsAlive:
case kAudioDevicePropertyDeviceIsRunning:
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
case kAudioDevicePropertyLatency:
case kAudioDevicePropertyStreams:
case kAudioObjectPropertyControlList:
case kAudioDevicePropertySafetyOffset:
case kAudioDevicePropertyNominalSampleRate:
case kAudioDevicePropertyAvailableNominalSampleRates:
case kAudioDevicePropertyIsHidden:
case kAudioDevicePropertyZeroTimeStampPeriod:
theAnswer = false;
break;
default:
theAnswer = BGM_Object::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 BGM_AbstractDevice::GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData) const
{
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioObjectPropertyName:
theAnswer = sizeof(CFStringRef);
break;
case kAudioObjectPropertyManufacturer:
theAnswer = sizeof(CFStringRef);
break;
case kAudioDevicePropertyDeviceUID:
theAnswer = sizeof(CFStringRef);
break;
case kAudioDevicePropertyModelUID:
theAnswer = sizeof(CFStringRef);
break;
case kAudioDevicePropertyTransportType:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyRelatedDevices:
theAnswer = sizeof(AudioObjectID);
break;
case kAudioDevicePropertyClockDomain:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceIsAlive:
theAnswer = sizeof(AudioClassID);
break;
case kAudioDevicePropertyDeviceIsRunning:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyLatency:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyStreams:
theAnswer = 0;
break;
case kAudioObjectPropertyControlList:
theAnswer = 0;
break;
case kAudioDevicePropertySafetyOffset:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyNominalSampleRate:
theAnswer = sizeof(Float64);
break;
case kAudioDevicePropertyAvailableNominalSampleRates:
theAnswer = 0;
break;
case kAudioDevicePropertyIsHidden:
theAnswer = sizeof(UInt32);
break;
case kAudioDevicePropertyZeroTimeStampPeriod:
theAnswer = sizeof(UInt32);
break;
default:
theAnswer = BGM_Object::GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
break;
};
return theAnswer;
}
void BGM_AbstractDevice::GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const
{
UInt32 theNumberItemsToFetch;
switch(inAddress.mSelector)
{
case kAudioObjectPropertyName:
case kAudioObjectPropertyManufacturer:
case kAudioDevicePropertyDeviceUID:
case kAudioDevicePropertyModelUID:
case kAudioDevicePropertyDeviceIsRunning:
case kAudioDevicePropertyZeroTimeStampPeriod:
case kAudioDevicePropertyNominalSampleRate:
case kAudioDevicePropertyAvailableNominalSampleRates:
// Crash debug builds if a concrete device delegates a required property that can't be
// handled here or in BGM_Object (the parent of this class).
//
// See BGM_Device for info about these properties.
//
// TODO: Write a test that checks all required properties for each subclass.
BGMAssert(false,
"BGM_AbstractDevice::GetPropertyData: Property %u not handled in subclass",
inAddress.mSelector);
case kAudioDevicePropertyTransportType:
// This value represents how the device is attached to the system. This can be
// any 32 bit integer, but common values for this property are defined in
// <CoreAudio/AudioHardwareBase.h>.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyTransportType for the device");
// Default to virtual device.
*reinterpret_cast<UInt32*>(outData) = kAudioDeviceTransportTypeVirtual;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyRelatedDevices:
// The related devices property identifies device objects that are very closely
// related. Generally, this is for relating devices that are packaged together
// in the hardware such as when the input side and the output side of a piece
// of hardware can be clocked separately and therefore need to be represented
// as separate AudioDevice objects. In such case, both devices would report
// that they are related to each other. Note that at minimum, a device is
// related to itself, so this list will always be at least one item long.
// Calculate the number of items that have been requested. Note that this
// number is allowed to be smaller than the actual size of the list. In such
// case, only that number of items will be returned
theNumberItemsToFetch = inDataSize / sizeof(AudioObjectID);
// Default to only have the one device.
if(theNumberItemsToFetch > 1)
{
theNumberItemsToFetch = 1;
}
// Write the devices' object IDs into the return value.
if(theNumberItemsToFetch > 0)
{
reinterpret_cast<AudioObjectID*>(outData)[0] = GetObjectID();
}
// Report how much we wrote.
outDataSize = theNumberItemsToFetch * sizeof(AudioObjectID);
break;
case kAudioDevicePropertyClockDomain:
// This property allows the device to declare what other devices it is
// synchronized with in hardware. The way it works is that if two devices have
// the same value for this property and the value is not zero, then the two
// devices are synchronized in hardware. Note that a device that either can't
// be synchronized with others or doesn't know should return 0 for this
// property.
//
// Default to 0.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyClockDomain for the device");
*reinterpret_cast<UInt32*>(outData) = 0;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceIsAlive:
// Default to alive.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceIsAlive for the device");
*reinterpret_cast<UInt32*>(outData) = 1;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
// This property returns whether or not the device wants to be able to be the
// default device for content. This is the device that iTunes and QuickTime
// will use to play their content on and FaceTime will use as it's microphone.
// Nearly all devices should allow for this.
//
// Default to true.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceCanBeDefaultDevice for the device");
*reinterpret_cast<UInt32*>(outData) = 1;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
// This property returns whether or not the device wants to be the system
// default device. This is the device that is used to play interface sounds and
// other incidental or UI-related sounds on. Most devices should allow this
// although devices with lots of latency may not want to.
//
// Default to true.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceCanBeDefaultSystemDevice for the device");
*reinterpret_cast<UInt32*>(outData) = 1;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyLatency:
// This property returns the presentation latency of the device. Default to 0.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyLatency for the device");
*reinterpret_cast<UInt32*>(outData) = 0;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyStreams:
// Default to not having any streams.
outDataSize = 0;
break;
case kAudioObjectPropertyControlList:
// Default to not having any controls.
outDataSize = 0;
break;
case kAudioDevicePropertySafetyOffset:
// This property returns the how close to now the HAL can read and write. Default to 0.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertySafetyOffset for the device");
*reinterpret_cast<UInt32*>(outData) = 0;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyIsHidden:
// This returns whether or not the device is visible to clients. Default to not hidden.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_AbstractDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyIsHidden for the device");
*reinterpret_cast<UInt32*>(outData) = 0;
outDataSize = sizeof(UInt32);
break;
default:
BGM_Object::GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
break;
};
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,111 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGM_AbstractDevice.h
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
#ifndef BGM_Driver__BGM_AbstractDevice
#define BGM_Driver__BGM_AbstractDevice
// SuperClass Includes
#include "BGM_Object.h"
#pragma clang assume_nonnull begin
class BGM_AbstractDevice
:
public BGM_Object
{
#pragma mark Construction/Destruction
protected:
BGM_AbstractDevice(AudioObjectID inObjectID,
AudioObjectID inOwnerObjectID);
virtual ~BGM_AbstractDevice();
#pragma mark Property Operations
public:
virtual bool HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
virtual bool IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
virtual UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData) const;
virtual void GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const;
#pragma mark IO Operations
public:
virtual void StartIO(UInt32 inClientID) = 0;
virtual void StopIO(UInt32 inClientID) = 0;
virtual void GetZeroTimeStamp(Float64& outSampleTime,
UInt64& outHostTime,
UInt64& outSeed) = 0;
virtual void WillDoIOOperation(UInt32 inOperationID,
bool& outWillDo,
bool& outWillDoInPlace) const = 0;
virtual void BeginIOOperation(UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
UInt32 inClientID) = 0;
virtual void DoIOOperation(AudioObjectID inStreamObjectID,
UInt32 inClientID, UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
void* ioMainBuffer,
void* __nullable ioSecondaryBuffer) = 0;
virtual void EndIOOperation(UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
UInt32 inClientID) = 0;
#pragma mark Implementation
public:
virtual CFStringRef CopyDeviceUID() const = 0;
virtual void AddClient(const AudioServerPlugInClientInfo* inClientInfo) = 0;
virtual void RemoveClient(const AudioServerPlugInClientInfo* inClientInfo) = 0;
virtual void PerformConfigChange(UInt64 inChangeAction,
void* __nullable inChangeInfo) = 0;
virtual void AbortConfigChange(UInt64 inChangeAction,
void* __nullable inChangeInfo) = 0;
};
#pragma clang assume_nonnull end
#endif /* BGM_Driver__BGM_AbstractDevice */

File diff suppressed because it is too large Load diff

View file

@ -17,24 +17,25 @@
// BGM_Device.h
// BGMDriver
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Based largely on SA_Device.h from Apple's SimpleAudioDriver Plug-In sample code.
// https://developer.apple.com/library/mac/samplecode/AudioDriverExamples
//
#ifndef __BGMDriver__BGM_Device__
#define __BGMDriver__BGM_Device__
#ifndef BGMDriver__BGM_Device
#define BGMDriver__BGM_Device
// SuperClass Includes
#include "BGM_Object.h"
#include "BGM_AbstractDevice.h"
// Local Includes
#include "BGM_Types.h"
#include "BGM_WrappedAudioEngine.h"
#include "BGM_Clients.h"
#include "BGM_TaskQueue.h"
#include "BGM_Stream.h"
// PublicUtility Includes
#include "CAMutex.h"
@ -42,11 +43,12 @@
// System Includes
#include <CoreFoundation/CoreFoundation.h>
#include <pthread.h>
class BGM_Device
:
public BGM_Object
public BGM_AbstractDevice
{
#pragma mark Construction/Destruction
@ -85,15 +87,6 @@ private:
void Device_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
void Device_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
#pragma mark Stream Property Operations
private:
bool Stream_HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
bool Stream_IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress) const;
UInt32 Stream_GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData) const;
void Stream_GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, UInt32& outDataSize, void* __nonnull outData) const;
void Stream_SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, const AudioObjectPropertyAddress& inAddress, UInt32 inQualifierDataSize, const void* __nullable inQualifierData, UInt32 inDataSize, const void* __nonnull inData);
#pragma mark Control Property Operations
private:
@ -125,6 +118,50 @@ private:
void UpdateAudibleStateSampleTimes_PostMix(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime, const void* __nonnull inBuffer);
void UpdateDeviceAudibleState(UInt32 inIOBufferFrameSize, Float64 inOutputSampleTime);
#pragma mark Accessors
public:
/*!
Enable or disable the device's volume and/or mute controls. This function is async because it
has to ask the host to stop IO for the device before the controls can be enabled/disabled.
See BGM_Device::PerformConfigChange and RequestDeviceConfigurationChange in AudioServerPlugIn.h.
*/
void RequestEnabledControls(bool inVolumeEnabled, bool inMuteEnabled);
Float64 GetSampleRate() const;
void RequestSampleRate(Float64 inRequestedSampleRate);
private:
/*!
Enable or disable the device's volume and/or mute controls.
Private because (after initialisation) this can only be called after asking the host to stop IO
for the device. See BGM_Device::RequestEnabledControls, BGM_Device::PerformConfigChange and
RequestDeviceConfigurationChange in AudioServerPlugIn.h.
*/
void SetEnabledControls(bool inVolumeEnabled, bool inMuteEnabled);
/*!
Set the device's sample rate.
Private because (after initialisation) this can only be called after asking the host to stop IO
for the device. See BGM_Device::RequestEnabledControls, BGM_Device::PerformConfigChange and
RequestDeviceConfigurationChange in AudioServerPlugIn.h.
@throws CAException if inNewSampleRate < 1 or if applying the sample rate to one of the streams
fails.
*/
void SetSampleRate(Float64 inNewSampleRate);
/*! @return True if inObjectID is the ID of one of this device's streams. */
bool IsStreamID(AudioObjectID inObjectID) const noexcept;
/*!
@return The stream that has the ID inObjectID and belongs to this device.
@throws CAException if there is no such stream (i.e. if inObjectID is neither
kObjectID_Stream_Input nor kObjectID_Stream_Output.)
*/
const BGM_Stream& GetStreamByID(AudioObjectID inObjectID) const;
#pragma mark Hardware Accessors
private:
@ -146,7 +183,12 @@ public:
CFStringRef __nonnull CopyDeviceUID() const { return CFSTR(kBGMDeviceUID); }
void AddClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
void RemoveClient(const AudioServerPlugInClientInfo* __nonnull inClientInfo);
/*!
Apply a change requested with BGM_PlugIn::Host_RequestDeviceConfigurationChange. See
PerformDeviceConfigurationChange in AudioServerPlugIn.h.
*/
void PerformConfigChange(UInt64 inChangeAction, void* __nullable inChangeInfo);
/*! Cancel a change requested with BGM_PlugIn::Host_RequestDeviceConfigurationChange. */
void AbortConfigChange(UInt64 inChangeAction, void* __nullable inChangeInfo);
private:
@ -164,9 +206,7 @@ private:
kNumberOfStreams = 2,
kNumberOfInputStreams = 1,
kNumberOfOutputStreams = 1,
kNumberOfControls = 2
kNumberOfOutputStreams = 1
};
CAMutex mStateMutex;
@ -174,7 +214,8 @@ private:
UInt64 __unused mSampleRateShadow; // Currently unused.
const Float64 kSampleRateDefault = 44100.0;
// Before we can change sample rate, the host has to stop the device. The new sample rate is stored here while it does.
// Before we can change sample rate, the host has to stop the device. The new sample rate is
// stored here while it does.
Float64 mPendingSampleRate;
BGM_WrappedAudioEngine* __nullable mWrappedAudioEngine;
@ -193,8 +234,8 @@ private:
UInt64 anchorHostTime = 0;
} mLoopbackTime;
bool mInputStreamIsActive;
bool mOutputStreamIsActive;
BGM_Stream mInputStream;
BGM_Stream mOutputStream;
SInt32 mDeviceAudibleState;
struct
@ -204,13 +245,24 @@ private:
Float64 latestAudibleMusic;
Float64 latestSilentMusic;
} mAudibleStateSampleTimes;
enum class ChangeAction : UInt64
{
SetSampleRate,
SetEnabledControls
};
// This volume range will be used when the BGMDevice isn't wrapping another device (or we fail to
// get the range of the wrapped device for some reason).
#define kDefaultMinRawVolumeValue 0
#define kDefaultMaxRawVolumeValue 96
#define kDefaultMinDbVolumeValue -96.0f
#define kDefaultMaxDbVolumeValue 0.0f
bool mOutputVolumeControlEnabled = true;
bool mOutputMuteControlEnabled = true;
bool mPendingOutputVolumeControlEnabled = true;
bool mPendingOutputMuteControlEnabled = true;
SInt32 mOutputMasterVolumeControlRawValueShadow;
SInt32 mOutputMasterMinRawVolumeShadow;
@ -222,5 +274,5 @@ private:
};
#endif /* __BGMDriver__BGM_Device__ */
#endif /* BGMDriver__BGM_Device */

View file

@ -0,0 +1,504 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGM_NullDevice.cpp
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
// Self Include
#include "BGM_NullDevice.h"
// Local Includes
#include "BGM_PlugIn.h"
// PublicUtility Includes
#include "CADebugMacros.h"
#include "CAException.h"
#include "CAPropertyAddress.h"
#include "CADispatchQueue.h"
// System Includes
#include <mach/mach_time.h> // For mach_absolute_time
#pragma clang assume_nonnull begin
static const Float64 kSampleRate = 44100.0;
static const UInt32 kZeroTimeStampPeriod = 10000; // Arbitrary.
#pragma mark Construction/Destruction
pthread_once_t BGM_NullDevice::sStaticInitializer = PTHREAD_ONCE_INIT;
BGM_NullDevice* BGM_NullDevice::sInstance = nullptr;
BGM_NullDevice& BGM_NullDevice::GetInstance()
{
pthread_once(&sStaticInitializer, StaticInitializer);
return *sInstance;
}
void BGM_NullDevice::StaticInitializer()
{
try
{
sInstance = new BGM_NullDevice;
// Note that we leave the device inactive initially. BGMApp will activate it when needed.
}
catch(...)
{
DebugMsg("BGM_NullDevice::StaticInitializer: Failed to create the device");
delete sInstance;
sInstance = nullptr;
}
}
BGM_NullDevice::BGM_NullDevice()
:
BGM_AbstractDevice(kObjectID_Device_Null, kAudioObjectPlugInObject),
mStateMutex("Null Device State"),
mIOMutex("Null Device IO"),
mStream(kObjectID_Stream_Null, kObjectID_Device_Null, false, kSampleRate)
{
}
BGM_NullDevice::~BGM_NullDevice()
{
}
void BGM_NullDevice::Activate()
{
CAMutex::Locker theStateLocker(mStateMutex);
if(!IsActive())
{
// Call the super-class, which just marks the object as active.
BGM_AbstractDevice::Activate();
// Calculate the host ticks per frame for the clock.
struct mach_timebase_info theTimeBaseInfo;
mach_timebase_info(&theTimeBaseInfo);
Float64 theHostClockFrequency = theTimeBaseInfo.denom / theTimeBaseInfo.numer;
theHostClockFrequency *= 1000000000.0;
mHostTicksPerFrame = theHostClockFrequency / kSampleRate;
SendDeviceIsAlivePropertyNotifications();
}
}
void BGM_NullDevice::Deactivate()
{
CAMutex::Locker theStateLocker(mStateMutex);
if(IsActive())
{
CAMutex::Locker theIOLocker(mIOMutex);
// Mark the object inactive by calling the super-class.
BGM_AbstractDevice::Deactivate();
SendDeviceIsAlivePropertyNotifications();
}
}
void BGM_NullDevice::SendDeviceIsAlivePropertyNotifications()
{
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[] = {
CAPropertyAddress(kAudioDevicePropertyDeviceIsAlive)
};
BGM_PlugIn::Host_PropertiesChanged(GetObjectID(), 1, theChangedProperties);
});
}
#pragma mark Property Operations
bool BGM_NullDevice::HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
if(inObjectID == mStream.GetObjectID())
{
return mStream.HasProperty(inObjectID, inClientPID, inAddress);
}
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioDevicePropertyDeviceCanBeDefaultDevice:
case kAudioDevicePropertyDeviceCanBeDefaultSystemDevice:
theAnswer = true;
break;
default:
theAnswer = BGM_AbstractDevice::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool BGM_NullDevice::IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
// Forward stream properties.
if(inObjectID == mStream.GetObjectID())
{
return mStream.IsPropertySettable(inObjectID, inClientPID, inAddress);
}
bool theAnswer = false;
switch(inAddress.mSelector)
{
default:
theAnswer = BGM_AbstractDevice::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 BGM_NullDevice::GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData) const
{
// Forward stream properties.
if(inObjectID == mStream.GetObjectID())
{
return mStream.GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
}
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioDevicePropertyStreams:
theAnswer = 1 * sizeof(AudioObjectID);
break;
case kAudioDevicePropertyAvailableNominalSampleRates:
theAnswer = 1 * sizeof(AudioValueRange);
break;
default:
theAnswer = BGM_AbstractDevice::GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
break;
};
return theAnswer;
}
void BGM_NullDevice::GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const
{
// Forward stream properties.
if(inObjectID == mStream.GetObjectID())
{
return mStream.GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
}
// See BGM_Device::Device_GetPropertyData for more information about these properties.
switch(inAddress.mSelector)
{
case kAudioObjectPropertyName:
ThrowIf(inDataSize < sizeof(AudioObjectID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioObjectPropertyName for the device");
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kNullDeviceName);
outDataSize = sizeof(CFStringRef);
break;
case kAudioObjectPropertyManufacturer:
ThrowIf(inDataSize < sizeof(AudioObjectID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioObjectPropertyManufacturer for the device");
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kNullDeviceManufacturerName);
outDataSize = sizeof(CFStringRef);
break;
case kAudioDevicePropertyDeviceUID:
ThrowIf(inDataSize < sizeof(AudioObjectID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceUID for the device");
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kBGMNullDeviceUID);
outDataSize = sizeof(CFStringRef);
break;
case kAudioDevicePropertyModelUID:
ThrowIf(inDataSize < sizeof(AudioObjectID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyModelUID for the device");
*reinterpret_cast<CFStringRef*>(outData) = CFSTR(kBGMNullDeviceModelUID);
outDataSize = sizeof(CFStringRef);
break;
case kAudioDevicePropertyDeviceIsAlive:
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceIsAlive for the device");
*reinterpret_cast<UInt32*>(outData) = IsActive() ? 1 : 0;
outDataSize = sizeof(UInt32);
break;
case kAudioDevicePropertyDeviceIsRunning:
{
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyDeviceIsRunning for the device");
CAMutex::Locker theStateLocker(mStateMutex);
// 1 means the device is running, i.e. doing IO.
*reinterpret_cast<UInt32*>(outData) = (mClientsDoingIO > 0) ? 1 : 0;
outDataSize = sizeof(UInt32);
}
break;
case kAudioDevicePropertyStreams:
if(inDataSize >= sizeof(AudioObjectID) &&
(inAddress.mScope == kAudioObjectPropertyScopeGlobal ||
inAddress.mScope == kAudioObjectPropertyScopeOutput))
{
// Return the ID of this device's stream.
reinterpret_cast<AudioObjectID*>(outData)[0] = kObjectID_Stream_Null;
// Report how much we wrote.
outDataSize = 1 * sizeof(AudioObjectID);
}
else
{
// Return nothing if we don't have a stream of the given scope or there's no room
// for the response.
outDataSize = 0;
}
break;
case kAudioDevicePropertyNominalSampleRate:
ThrowIf(inDataSize < sizeof(Float64),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyNominalSampleRate for the device");
*reinterpret_cast<Float64*>(outData) = kSampleRate;
outDataSize = sizeof(Float64);
break;
case kAudioDevicePropertyAvailableNominalSampleRates:
// Check we were given space to return something.
if((inDataSize / sizeof(AudioValueRange)) >= 1)
{
// This device doesn't support changing the sample rate.
reinterpret_cast<AudioValueRange*>(outData)[0].mMinimum = kSampleRate;
reinterpret_cast<AudioValueRange*>(outData)[0].mMaximum = kSampleRate;
outDataSize = sizeof(AudioValueRange);
}
else
{
outDataSize = 0;
}
break;
case kAudioDevicePropertyZeroTimeStampPeriod:
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_NullDevice::GetPropertyData: not enough space for the return value of "
"kAudioDevicePropertyZeroTimeStampPeriod for the device");
*reinterpret_cast<UInt32*>(outData) = kZeroTimeStampPeriod;
outDataSize = sizeof(UInt32);
break;
default:
BGM_AbstractDevice::GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
break;
};
}
void BGM_NullDevice::SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
const void* inData)
{
// This device doesn't have any settable properties, so just pass stream properties along.
if(inObjectID == mStream.GetObjectID())
{
mStream.SetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
inData);
}
}
#pragma mark IO Operations
void BGM_NullDevice::StartIO(UInt32 inClientID)
{
#pragma unused (inClientID)
CAMutex::Locker theStateLocker(mStateMutex);
if(mClientsDoingIO == 0)
{
// Reset the clock.
mNumberTimeStamps = 0;
mAnchorHostTime = mach_absolute_time();
// Send notifications.
DebugMsg("BGM_NullDevice::StartIO: Sending kAudioDevicePropertyDeviceIsRunning");
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperty[] = {
CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning)
};
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device_Null, 1, theChangedProperty);
});
}
mClientsDoingIO++;
}
void BGM_NullDevice::StopIO(UInt32 inClientID)
{
#pragma unused (inClientID)
CAMutex::Locker theStateLocker(mStateMutex);
ThrowIf(mClientsDoingIO == 0,
CAException(kAudioHardwareIllegalOperationError),
"BGM_NullDevice::StopIO: Underflowed mClientsDoingIO");
mClientsDoingIO--;
if(mClientsDoingIO == 0)
{
// Send notifications.
DebugMsg("BGM_NullDevice::StopIO: Sending kAudioDevicePropertyDeviceIsRunning");
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperty[] = {
CAPropertyAddress(kAudioDevicePropertyDeviceIsRunning)
};
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device_Null, 1, theChangedProperty);
});
}
}
void BGM_NullDevice::GetZeroTimeStamp(Float64& outSampleTime,
UInt64& outHostTime,
UInt64& outSeed)
{
CAMutex::Locker theIOLocker(mIOMutex);
// Not sure whether there's actually any point to implementing this. The documentation says that
// clockless devices don't need to, but if the device doesn't have
// kAudioDevicePropertyZeroTimeStampPeriod the HAL seems to reject it. So we give it a simple
// clock similar to the loopback clock in BGM_Device.
UInt64 theCurrentHostTime = mach_absolute_time();
// Calculate the next host time.
Float64 theHostTicksPerPeriod = mHostTicksPerFrame * static_cast<Float64>(kZeroTimeStampPeriod);
Float64 theHostTickOffset = static_cast<Float64>(mNumberTimeStamps + 1) * theHostTicksPerPeriod;
UInt64 theNextHostTime = mAnchorHostTime + static_cast<UInt64>(theHostTickOffset);
// Go to the next period if the next host time is less than the current time.
if(theNextHostTime <= theCurrentHostTime)
{
mNumberTimeStamps++;
}
Float64 theHostTicksSinceAnchor =
(static_cast<Float64>(mNumberTimeStamps) * theHostTicksPerPeriod);
// Set the return values.
outSampleTime = mNumberTimeStamps * kZeroTimeStampPeriod;
outHostTime = static_cast<UInt64>(mAnchorHostTime + theHostTicksSinceAnchor);
outSeed = 1;
}
void BGM_NullDevice::WillDoIOOperation(UInt32 inOperationID,
bool& outWillDo,
bool& outWillDoInPlace) const
{
switch(inOperationID)
{
case kAudioServerPlugInIOOperationWriteMix:
outWillDo = true;
outWillDoInPlace = true;
break;
default:
outWillDo = false;
outWillDoInPlace = true;
break;
};
}
void BGM_NullDevice::DoIOOperation(AudioObjectID inStreamObjectID,
UInt32 inClientID,
UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
void* ioMainBuffer,
void* __nullable ioSecondaryBuffer)
{
#pragma unused (inStreamObjectID, inClientID, inOperationID, inIOCycleInfo, inIOBufferFrameSize)
#pragma unused (ioMainBuffer, ioSecondaryBuffer)
// Ignore the audio data.
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,186 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
//
// BGM_NullDevice.h
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
// A device with one output stream that ignores any audio played on that stream.
//
// If we change BGMDevice's controls list, to match the output device set in BGMApp, we need to
// change the OS X default device so other programs (including the OS X audio UI) will update
// themselves. We could just change to the real output device and change back, but that could have
// side effects the user wouldn't expect. For example, an app the user had muted might be unmuted
// for a short period.
//
// Instead, BGMApp temporarily enables this device and uses it to toggle the default device. This
// device is disabled at all other times so it can be hidden from the user. (We can't just use
// kAudioDevicePropertyIsHidden because hidden devices can't be default and the HAL doesn't seem to
// let devices change kAudioDevicePropertyIsHidden after setting it initially.)
//
// It might be worth eventually having a virtual device for each real output device, but this is
// simpler and seems to work well enough for now.
//
#ifndef BGMDriver__BGM_NullDevice
#define BGMDriver__BGM_NullDevice
// SuperClass Includes
#include "BGM_AbstractDevice.h"
// Local Includes
#include "BGM_Types.h"
#include "BGM_Stream.h"
// PublicUtility Includes
#include "CAMutex.h"
// System Includes
#include <pthread.h>
#pragma clang assume_nonnull begin
class BGM_NullDevice
:
public BGM_AbstractDevice
{
#pragma mark Construction/Destruction
public:
static BGM_NullDevice& GetInstance();
private:
static void StaticInitializer();
protected:
BGM_NullDevice();
virtual ~BGM_NullDevice();
public:
virtual void Activate();
virtual void Deactivate();
private:
void SendDeviceIsAlivePropertyNotifications();
#pragma mark Property Operations
public:
bool HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
bool IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData) const;
void GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const;
void SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* inQualifierData,
UInt32 inDataSize,
const void* inData);
#pragma mark IO Operations
public:
void StartIO(UInt32 inClientID);
void StopIO(UInt32 inClientID);
void GetZeroTimeStamp(Float64& outSampleTime,
UInt64& outHostTime,
UInt64& outSeed);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
void WillDoIOOperation(UInt32 inOperationID,
bool& outWillDo,
bool& outWillDoInPlace) const;
void BeginIOOperation(UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
UInt32 inClientID)
{ /* No-op */ };
void DoIOOperation(AudioObjectID inStreamObjectID,
UInt32 inClientID,
UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
void* ioMainBuffer,
void* __nullable ioSecondaryBuffer);
void EndIOOperation(UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo& inIOCycleInfo,
UInt32 inClientID)
{ /* No-op */ };
#pragma mark Implementation
public:
CFStringRef CopyDeviceUID() const
{ return CFSTR(kBGMNullDeviceUID); };
void AddClient(const AudioServerPlugInClientInfo* inClientInfo)
{ /* No-op */ };
void RemoveClient(const AudioServerPlugInClientInfo* inClientInfo)
{ /* No-op */ };
void PerformConfigChange(UInt64 inChangeAction,
void* __nullable inChangeInfo)
{ /* No-op */ };
void AbortConfigChange(UInt64 inChangeAction,
void* __nullable inChangeInfo)
{ /* No-op */ };
#pragma clang diagnostic pop
private:
static pthread_once_t sStaticInitializer;
static BGM_NullDevice* sInstance;
#define kNullDeviceName "Background Music Null Device"
#define kNullDeviceManufacturerName \
"Background Music contributors"
CAMutex mStateMutex;
CAMutex mIOMutex;
BGM_Stream mStream;
UInt32 mClientsDoingIO = 0;
Float64 mHostTicksPerFrame = 0.0;
UInt64 mNumberTimeStamps = 0;
UInt64 mAnchorHostTime = 0;
};
#pragma clang assume_nonnull end
#endif /* BGMDriver__BGM_NullDevice */

View file

@ -17,7 +17,7 @@
// BGM_PlugIn.cpp
// BGMDriver
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Based largely on SA_PlugIn.cpp from Apple's SimpleAudioDriver Plug-In sample code.
@ -28,13 +28,14 @@
#include "BGM_PlugIn.h"
// Local Includes
#include "BGM_Types.h"
#include "BGM_Device.h"
#include "BGM_NullDevice.h"
// PublicUtility Includes
#include "CAMutex.h"
#include "CAException.h"
#include "CADebugMacros.h"
#include "CAPropertyAddress.h"
#include "CADispatchQueue.h"
#pragma mark Construction/Destruction
@ -93,7 +94,9 @@ bool BGM_PlugIn::HasProperty(AudioObjectID inObjectID, pid_t inClientPID, const
case kAudioObjectPropertyManufacturer:
case kAudioPlugInPropertyDeviceList:
case kAudioPlugInPropertyTranslateUIDToDevice:
case kAudioPlugInPropertyResourceBundle:
case kAudioPlugInPropertyResourceBundle:
case kAudioObjectPropertyCustomPropertyInfoList:
case kAudioPlugInCustomPropertyNullDeviceActive:
theAnswer = true;
break;
@ -111,9 +114,14 @@ bool BGM_PlugIn::IsPropertySettable(AudioObjectID inObjectID, pid_t inClientPID,
case kAudioObjectPropertyManufacturer:
case kAudioPlugInPropertyDeviceList:
case kAudioPlugInPropertyTranslateUIDToDevice:
case kAudioPlugInPropertyResourceBundle:
case kAudioPlugInPropertyResourceBundle:
case kAudioObjectPropertyCustomPropertyInfoList:
theAnswer = false;
break;
case kAudioPlugInCustomPropertyNullDeviceActive:
theAnswer = true;
break;
default:
theAnswer = BGM_Object::IsPropertySettable(inObjectID, inClientPID, inAddress);
@ -132,7 +140,7 @@ UInt32 BGM_PlugIn::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientP
case kAudioObjectPropertyOwnedObjects:
case kAudioPlugInPropertyDeviceList:
theAnswer = sizeof(AudioObjectID);
theAnswer = (BGM_NullDevice::GetInstance().IsActive() ? 2 : 1) * sizeof(AudioObjectID);
break;
case kAudioPlugInPropertyTranslateUIDToDevice:
@ -142,6 +150,14 @@ UInt32 BGM_PlugIn::GetPropertyDataSize(AudioObjectID inObjectID, pid_t inClientP
case kAudioPlugInPropertyResourceBundle:
theAnswer = sizeof(CFStringRef);
break;
case kAudioObjectPropertyCustomPropertyInfoList:
theAnswer = sizeof(AudioServerPlugInCustomPropertyInfo);
break;
case kAudioPlugInCustomPropertyNullDeviceActive:
theAnswer = sizeof(CFBooleanRef);
break;
default:
theAnswer = BGM_Object::GetPropertyDataSize(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData);
@ -161,16 +177,22 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
break;
case kAudioObjectPropertyOwnedObjects:
// Fall through because this plug-in object only owns the devices.
case kAudioPlugInPropertyDeviceList:
{
// This plug-in object only owns the device
AudioObjectID* theReturnedDeviceList = reinterpret_cast<AudioObjectID*>(outData);
if(inDataSize >= sizeof(AudioObjectID))
if((inDataSize >= 2 * sizeof(AudioObjectID)) && BGM_NullDevice::GetInstance().IsActive())
{
theReturnedDeviceList[0] = kObjectID_Device;
theReturnedDeviceList[1] = kObjectID_Device_Null;
// say how much we returned
outDataSize = sizeof(AudioObjectID);
outDataSize = 2 * sizeof(AudioObjectID);
}
else if(inDataSize >= sizeof(AudioObjectID))
{
theReturnedDeviceList[0] = kObjectID_Device;
outDataSize = sizeof(AudioObjectID);
}
else
{
@ -186,9 +208,30 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
// has the UID.
ThrowIf(inQualifierDataSize < sizeof(CFStringRef), CAException(kAudioHardwareBadPropertySizeError), "BGM_PlugIn::GetPropertyData: the qualifier size is too small for kAudioPlugInPropertyTranslateUIDToDevice");
ThrowIf(inDataSize < sizeof(AudioObjectID), CAException(kAudioHardwareBadPropertySizeError), "BGM_PlugIn::GetPropertyData: not enough space for the return value of kAudioPlugInPropertyTranslateUIDToDevice");
CFStringRef theUID = *reinterpret_cast<const CFStringRef*>(inQualifierData);
*reinterpret_cast<AudioObjectID*>(outData) =
CFEqual(theUID, BGM_Device::GetInstance().CopyDeviceUID()) ? kObjectID_Device : kAudioObjectUnknown;
AudioObjectID* outID = reinterpret_cast<AudioObjectID*>(outData);
if(CFEqual(theUID, BGM_Device::GetInstance().CopyDeviceUID()))
{
DebugMsg("BGM_PlugIn::GetPropertyData: Returning BGMDevice for "
"kAudioPlugInPropertyTranslateUIDToDevice");
*outID = kObjectID_Device;
}
else if(BGM_NullDevice::GetInstance().IsActive() &&
CFEqual(theUID, BGM_NullDevice::GetInstance().CopyDeviceUID()))
{
DebugMsg("BGM_PlugIn::GetPropertyData: Returning null device for "
"kAudioPlugInPropertyTranslateUIDToDevice");
*outID = kObjectID_Device_Null;
}
else
{
LogWarning("BGM_PlugIn::GetPropertyData: Returning kAudioObjectUnknown for "
"kAudioPlugInPropertyTranslateUIDToDevice");
*outID = kAudioObjectUnknown;
}
outDataSize = sizeof(AudioObjectID);
}
break;
@ -201,7 +244,37 @@ void BGM_PlugIn::GetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
*reinterpret_cast<CFStringRef*>(outData) = CFSTR("");
outDataSize = sizeof(CFStringRef);
break;
case kAudioObjectPropertyCustomPropertyInfoList:
if(inDataSize >= sizeof(AudioServerPlugInCustomPropertyInfo))
{
AudioServerPlugInCustomPropertyInfo* outCustomProperties =
reinterpret_cast<AudioServerPlugInCustomPropertyInfo*>(outData);
outCustomProperties[0].mSelector = kAudioPlugInCustomPropertyNullDeviceActive;
outCustomProperties[0].mPropertyDataType =
kAudioServerPlugInCustomPropertyDataTypeCFPropertyList;
outCustomProperties[0].mQualifierDataType =
kAudioServerPlugInCustomPropertyDataTypeNone;
outDataSize = sizeof(AudioServerPlugInCustomPropertyInfo);
}
else
{
outDataSize = 0;
}
break;
case kAudioPlugInCustomPropertyNullDeviceActive:
ThrowIf(inDataSize < sizeof(CFBooleanRef),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_PlugIn::GetPropertyData: not enough space for the return value of "
"kAudioPlugInCustomPropertyNullDeviceActive");
*reinterpret_cast<CFBooleanRef*>(outData) =
BGM_NullDevice::GetInstance().IsActive() ? kCFBooleanTrue : kCFBooleanFalse;
outDataSize = sizeof(CFBooleanRef);
break;
default:
BGM_Object::GetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, outDataSize, outData);
break;
@ -212,6 +285,55 @@ void BGM_PlugIn::SetPropertyData(AudioObjectID inObjectID, pid_t inClientPID, co
{
switch(inAddress.mSelector)
{
case kAudioPlugInCustomPropertyNullDeviceActive:
{
ThrowIf(inDataSize < sizeof(CFBooleanRef),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_PlugIn::SetPropertyData: wrong size for the data for "
"kAudioPlugInCustomPropertyNullDeviceActive");
CFBooleanRef theIsActiveRef = *reinterpret_cast<const CFBooleanRef*>(inData);
ThrowIfNULL(theIsActiveRef,
CAException(kAudioHardwareIllegalOperationError),
"BGM_PlugIn::SetPropertyData: null reference given for "
"kAudioPlugInCustomPropertyNullDeviceActive");
ThrowIf(CFGetTypeID(theIsActiveRef) != CFBooleanGetTypeID(),
CAException(kAudioHardwareIllegalOperationError),
"BGM_PlugIn::SetPropertyData: CFType given for "
"kAudioPlugInCustomPropertyNullDeviceActive was not a CFBoolean");
bool theIsActive = CFBooleanGetValue(theIsActiveRef);
if(theIsActive != BGM_NullDevice::GetInstance().IsActive())
{
// Activate/deactivate the Null Device. We only make it active for a short
// period, while changing output device in BGMApp, so it can be hidden from the
// user.
if(theIsActive)
{
DebugMsg("BGM_PlugIn::SetPropertyData: Activating null device");
BGM_NullDevice::GetInstance().Activate();
}
else
{
DebugMsg("BGM_PlugIn::SetPropertyData: Deactivating null device");
BGM_NullDevice::GetInstance().Deactivate();
}
// Send notifications.
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[] = {
CAPropertyAddress(kAudioObjectPropertyOwnedObjects),
CAPropertyAddress(kAudioPlugInPropertyDeviceList)
};
Host_PropertiesChanged(GetObjectID(), 2, theChangedProperties);
});
}
}
break;
default:
BGM_Object::SetPropertyData(inObjectID, inClientPID, inAddress, inQualifierDataSize, inQualifierData, inDataSize, inData);
break;

View file

@ -17,7 +17,7 @@
// BGM_PlugInInterface.cpp
// BGMDriver
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Based largely on SA_PlugIn.cpp from Apple's SimpleAudioDriver Plug-In sample code.
@ -36,6 +36,7 @@
#include "BGM_Object.h"
#include "BGM_PlugIn.h"
#include "BGM_Device.h"
#include "BGM_NullDevice.h"
#pragma mark COM Prototypes
@ -110,12 +111,31 @@ static BGM_Object& BGM_LookUpOwnerObject(AudioObjectID inObjectID)
case kObjectID_Volume_Output_Master:
case kObjectID_Mute_Output_Master:
return BGM_Device::GetInstance();
case kObjectID_Device_Null:
case kObjectID_Stream_Null:
return BGM_NullDevice::GetInstance();
}
DebugMsg("BGM_LookUpOwnerObject: unknown object");
Throw(CAException(kAudioHardwareBadObjectError));
}
static BGM_AbstractDevice& BGM_LookUpDevice(AudioObjectID inObjectID)
{
switch(inObjectID)
{
case kObjectID_Device:
return BGM_Device::GetInstance();
case kObjectID_Device_Null:
return BGM_NullDevice::GetInstance();
}
DebugMsg("BGM_LookUpDevice: unknown device");
Throw(CAException(kAudioHardwareBadDeviceError));
}
#pragma mark Factory
extern "C"
@ -246,14 +266,17 @@ static OSStatus BGM_Initialize(AudioServerPlugInDriverRef inDriver, AudioServerP
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_Initialize: bad driver reference");
// Check the arguments.
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_Initialize: bad driver reference");
// store the AudioServerPlugInHostRef
// Store the AudioServerPlugInHostRef.
BGM_PlugIn::GetInstance().SetHost(inHost);
// Init/activate the device
// Init/activate the devices.
BGM_Device::GetInstance();
BGM_NullDevice::GetInstance();
}
catch(const CAException& inException)
{
@ -298,12 +321,16 @@ static OSStatus BGM_AddDeviceClient(AudioServerPlugInDriverRef inDriver, AudioOb
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_AddDeviceClient: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_AddDeviceClient: unknown device");
// Check the arguments.
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_AddDeviceClient: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadObjectError),
"BGM_AddDeviceClient: unknown device");
// inform the device
BGM_Device::GetInstance().AddClient(inClientInfo);
// Inform the device.
BGM_LookUpDevice(inDeviceObjectID).AddClient(inClientInfo);
}
catch(const CAException& inException)
{
@ -330,12 +357,16 @@ static OSStatus BGM_RemoveDeviceClient(AudioServerPlugInDriverRef inDriver, Audi
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_RemoveDeviceClient: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_RemoveDeviceClient: unknown device");
// Check the arguments.
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_RemoveDeviceClient: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadObjectError),
"BGM_RemoveDeviceClient: unknown device");
// inform the device
BGM_Device::GetInstance().RemoveClient(inClientInfo);
// Inform the device.
BGM_LookUpDevice(inDeviceObjectID).RemoveClient(inClientInfo);
}
catch(const CAException& inException)
{
@ -364,17 +395,21 @@ static OSStatus BGM_PerformDeviceConfigurationChange(AudioServerPlugInDriverRef
// also handle figuring out exactly what changed for the non-control related properties. This
// means that the only notifications that would need to be sent here would be for either
// custom properties the HAL doesn't know about or for controls.
OSStatus theAnswer = 0;
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_PerformDeviceConfigurationChange: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_PerformDeviceConfigurationChange: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_PerformDeviceConfigurationChange: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_PerformDeviceConfigurationChange: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().PerformConfigChange(inChangeAction, inChangeInfo);
BGM_LookUpDevice(inDeviceObjectID).PerformConfigChange(inChangeAction, inChangeInfo);
}
catch(const CAException& inException)
{
@ -398,11 +433,15 @@ static OSStatus BGM_AbortDeviceConfigurationChange(AudioServerPlugInDriverRef in
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_PerformDeviceConfigurationChange: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_PerformDeviceConfigurationChange: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_PerformDeviceConfigurationChange: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_PerformDeviceConfigurationChange: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().AbortConfigChange(inChangeAction, inChangeInfo);
BGM_LookUpDevice(inDeviceObjectID).AbortConfigChange(inChangeAction, inChangeInfo);
}
catch(const CAException& inException)
{
@ -438,6 +477,9 @@ static Boolean BGM_HasProperty(AudioServerPlugInDriverRef inDriver, AudioObjectI
}
catch(...)
{
LogError("BGM_PlugInInterface::BGM_HasProperty: unknown exception. (object: %u, address: %u)",
inObjectID,
inAddress ? inAddress->mSelector : 0);
theAnswer = false;
}
@ -474,6 +516,9 @@ static OSStatus BGM_IsPropertySettable(AudioServerPlugInDriverRef inDriver, Audi
}
catch(...)
{
LogError("BGM_PlugInInterface::BGM_IsPropertySettable: unknown exception. (object: %u, address: %u)",
inObjectID,
inAddress ? inAddress->mSelector : 0);
theAnswer = kAudioHardwareUnspecifiedError;
}
@ -509,6 +554,9 @@ static OSStatus BGM_GetPropertyDataSize(AudioServerPlugInDriverRef inDriver, Aud
}
catch(...)
{
LogError("BGM_PlugInInterface::BGM_GetPropertyDataSize: unknown exception. (object: %u, address: %u)",
inObjectID,
inAddress ? inAddress->mSelector : 0);
theAnswer = kAudioHardwareUnspecifiedError;
}
@ -545,6 +593,9 @@ static OSStatus BGM_GetPropertyData(AudioServerPlugInDriverRef inDriver, AudioOb
}
catch(...)
{
LogError("BGM_PlugInInterface::BGM_GetPropertyData: unknown exception. (object: %u, address: %u)",
inObjectID,
inAddress ? inAddress->mSelector : 0);
theAnswer = kAudioHardwareUnspecifiedError;
}
@ -556,7 +607,7 @@ static OSStatus BGM_SetPropertyData(AudioServerPlugInDriverRef inDriver, AudioOb
// This method changes the value of the given property
OSStatus theAnswer = 0;
try
{
// check the arguments
@ -587,6 +638,9 @@ static OSStatus BGM_SetPropertyData(AudioServerPlugInDriverRef inDriver, AudioOb
}
catch(...)
{
LogError("BGM_PlugInInterface::BGM_SetPropertyData: unknown exception. (object: %u, address: %u)",
inObjectID,
inAddress ? inAddress->mSelector : 0);
theAnswer = kAudioHardwareUnspecifiedError;
}
@ -595,7 +649,9 @@ static OSStatus BGM_SetPropertyData(AudioServerPlugInDriverRef inDriver, AudioOb
#pragma mark IO Operations
static OSStatus BGM_StartIO(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, UInt32 inClientID)
static OSStatus BGM_StartIO(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
UInt32 inClientID)
{
// This call tells the device that IO is starting for the given client. When this routine
// returns, the device's clock is running and it is ready to have data read/written. It is
@ -608,11 +664,15 @@ static OSStatus BGM_StartIO(AudioServerPlugInDriverRef inDriver, AudioObjectID i
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_StartIO: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_StartIO: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_StartIO: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_StartIO: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().StartIO(inClientID);
BGM_LookUpDevice(inDeviceObjectID).StartIO(inClientID);
}
catch(const CAException& inException)
{
@ -626,7 +686,9 @@ static OSStatus BGM_StartIO(AudioServerPlugInDriverRef inDriver, AudioObjectID i
return theAnswer;
}
static OSStatus BGM_StopIO(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, UInt32 inClientID)
static OSStatus BGM_StopIO(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
UInt32 inClientID)
{
// This call tells the device that the client has stopped IO. The driver can stop the hardware
// once all clients have stopped.
@ -636,11 +698,15 @@ static OSStatus BGM_StopIO(AudioServerPlugInDriverRef inDriver, AudioObjectID in
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_StopIO: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_StopIO: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_StopIO: bad driver reference");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_StopIO: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().StopIO(inClientID);
BGM_LookUpDevice(inDeviceObjectID).StopIO(inClientID);
}
catch(const CAException& inException)
{
@ -654,29 +720,43 @@ static OSStatus BGM_StopIO(AudioServerPlugInDriverRef inDriver, AudioObjectID in
return theAnswer;
}
static OSStatus BGM_GetZeroTimeStamp(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, UInt32 inClientID, Float64* outSampleTime, UInt64* outHostTime, UInt64* outSeed)
static OSStatus BGM_GetZeroTimeStamp(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
UInt32 inClientID,
Float64* outSampleTime,
UInt64* outHostTime,
UInt64* outSeed)
{
#pragma unused(inClientID)
// This method returns the current zero time stamp for the device. The HAL models the timing of
// a device as a series of time stamps that relate the sample time to a host time. The zero
// time stamps are spaced such that the sample times are the value of
// kAudioDevicePropertyZeroTimeStampPeriod apart. This is often modeled using a ring buffer
// where the zero time stamp is updated when wrapping around the ring buffer.
#pragma unused(inClientID)
OSStatus theAnswer = 0;
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_GetZeroTimeStamp: bad driver reference");
ThrowIfNULL(outSampleTime, CAException(kAudioHardwareIllegalOperationError), "BGM_GetZeroTimeStamp: no place to put the sample time");
ThrowIfNULL(outHostTime, CAException(kAudioHardwareIllegalOperationError), "BGM_GetZeroTimeStamp: no place to put the host time");
ThrowIfNULL(outSeed, CAException(kAudioHardwareIllegalOperationError), "BGM_GetZeroTimeStamp: no place to put the seed");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_GetZeroTimeStamp: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_GetZeroTimeStamp: bad driver reference");
ThrowIfNULL(outSampleTime,
CAException(kAudioHardwareIllegalOperationError),
"BGM_GetZeroTimeStamp: no place to put the sample time");
ThrowIfNULL(outHostTime,
CAException(kAudioHardwareIllegalOperationError),
"BGM_GetZeroTimeStamp: no place to put the host time");
ThrowIfNULL(outSeed,
CAException(kAudioHardwareIllegalOperationError),
"BGM_GetZeroTimeStamp: no place to put the seed");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_GetZeroTimeStamp: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().GetZeroTimeStamp(*outSampleTime, *outHostTime, *outSeed);
BGM_LookUpDevice(inDeviceObjectID).GetZeroTimeStamp(*outSampleTime, *outHostTime, *outSeed);
}
catch(const CAException& inException)
{
@ -690,26 +770,38 @@ static OSStatus BGM_GetZeroTimeStamp(AudioServerPlugInDriverRef inDriver, AudioO
return theAnswer;
}
static OSStatus BGM_WillDoIOOperation(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, UInt32 inClientID, UInt32 inOperationID, Boolean* outWillDo, Boolean* outWillDoInPlace)
static OSStatus BGM_WillDoIOOperation(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
UInt32 inClientID,
UInt32 inOperationID,
Boolean* outWillDo,
Boolean* outWillDoInPlace)
{
// This method returns whether or not the device will do a given IO operation.
#pragma unused(inClientID)
// This method returns whether or not the device will do a given IO operation.
OSStatus theAnswer = 0;
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_WillDoIOOperation: bad driver reference");
ThrowIfNULL(outWillDo, CAException(kAudioHardwareIllegalOperationError), "BGM_WillDoIOOperation: no place to put the will-do return value");
ThrowIfNULL(outWillDoInPlace, CAException(kAudioHardwareIllegalOperationError), "BGM_WillDoIOOperation: no place to put the in-place return value");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_WillDoIOOperation: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_WillDoIOOperation: bad driver reference");
ThrowIfNULL(outWillDo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_WillDoIOOperation: no place to put the will-do return value");
ThrowIfNULL(outWillDoInPlace,
CAException(kAudioHardwareIllegalOperationError),
"BGM_WillDoIOOperation: no place to put the in-place return value");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_WillDoIOOperation: unknown device");
// tell the device to do the work
bool willDo = false;
bool willDoInPlace = false;
BGM_Device::GetInstance().WillDoIOOperation(inOperationID, willDo, willDoInPlace);
BGM_LookUpDevice(inDeviceObjectID).WillDoIOOperation(inOperationID, willDo, willDoInPlace);
// set the return values
*outWillDo = willDo;
@ -727,7 +819,12 @@ static OSStatus BGM_WillDoIOOperation(AudioServerPlugInDriverRef inDriver, Audio
return theAnswer;
}
static OSStatus BGM_BeginIOOperation(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, UInt32 inClientID, UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo* inIOCycleInfo)
static OSStatus BGM_BeginIOOperation(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
UInt32 inClientID,
UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo* inIOCycleInfo)
{
// This is called at the beginning of an IO operation.
@ -736,12 +833,21 @@ static OSStatus BGM_BeginIOOperation(AudioServerPlugInDriverRef inDriver, AudioO
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_BeginIOOperation: bad driver reference");
ThrowIfNULL(inIOCycleInfo, CAException(kAudioHardwareIllegalOperationError), "BGM_BeginIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_BeginIOOperation: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_BeginIOOperation: bad driver reference");
ThrowIfNULL(inIOCycleInfo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_BeginIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_BeginIOOperation: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().BeginIOOperation(inOperationID, inIOBufferFrameSize, *inIOCycleInfo, inClientID);
BGM_LookUpDevice(inDeviceObjectID).BeginIOOperation(inOperationID,
inIOBufferFrameSize,
*inIOCycleInfo,
inClientID);
}
catch(const CAException& inException)
{
@ -749,27 +855,50 @@ static OSStatus BGM_BeginIOOperation(AudioServerPlugInDriverRef inDriver, AudioO
}
catch(...)
{
DebugMsg("BGM_PlugInInterface::BGM_BeginIOOperation: unknown exception. (device: %s, operation: %u)",
(inDeviceObjectID == kObjectID_Device ? "BGMDevice" : "other"),
inOperationID);
theAnswer = kAudioHardwareUnspecifiedError;
}
return theAnswer;
}
static OSStatus BGM_DoIOOperation(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, AudioObjectID inStreamObjectID, UInt32 inClientID, UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo* inIOCycleInfo, void* ioMainBuffer, void* ioSecondaryBuffer)
static OSStatus BGM_DoIOOperation(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
AudioObjectID inStreamObjectID,
UInt32 inClientID,
UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo* inIOCycleInfo,
void* ioMainBuffer,
void* ioSecondaryBuffer)
{
// This is called to actually perform a given operation.
// This is called to actually perform a given IO operation.
OSStatus theAnswer = 0;
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_EndIOOperation: bad driver reference");
ThrowIfNULL(inIOCycleInfo, CAException(kAudioHardwareIllegalOperationError), "BGM_EndIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_EndIOOperation: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_EndIOOperation: bad driver reference");
ThrowIfNULL(inIOCycleInfo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_EndIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_EndIOOperation: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().DoIOOperation(inStreamObjectID, inClientID, inOperationID, inIOBufferFrameSize, *inIOCycleInfo, ioMainBuffer, ioSecondaryBuffer);
BGM_LookUpDevice(inDeviceObjectID).DoIOOperation(inStreamObjectID,
inClientID,
inOperationID,
inIOBufferFrameSize,
*inIOCycleInfo,
ioMainBuffer,
ioSecondaryBuffer);
}
catch(const CAException& inException)
{
@ -777,13 +906,21 @@ static OSStatus BGM_DoIOOperation(AudioServerPlugInDriverRef inDriver, AudioObje
}
catch(...)
{
DebugMsg("BGM_PlugInInterface::BGM_DoIOOperation: unknown exception. (device: %s, operation: %u)",
(inDeviceObjectID == kObjectID_Device ? "BGMDevice" : "other"),
inOperationID);
theAnswer = kAudioHardwareUnspecifiedError;
}
return theAnswer;
}
static OSStatus BGM_EndIOOperation(AudioServerPlugInDriverRef inDriver, AudioObjectID inDeviceObjectID, UInt32 inClientID, UInt32 inOperationID, UInt32 inIOBufferFrameSize, const AudioServerPlugInIOCycleInfo* inIOCycleInfo)
static OSStatus BGM_EndIOOperation(AudioServerPlugInDriverRef inDriver,
AudioObjectID inDeviceObjectID,
UInt32 inClientID,
UInt32 inOperationID,
UInt32 inIOBufferFrameSize,
const AudioServerPlugInIOCycleInfo* inIOCycleInfo)
{
// This is called at the end of an IO operation.
@ -792,12 +929,21 @@ static OSStatus BGM_EndIOOperation(AudioServerPlugInDriverRef inDriver, AudioObj
try
{
// check the arguments
ThrowIf(inDriver != gAudioServerPlugInDriverRef, CAException(kAudioHardwareBadObjectError), "BGM_EndIOOperation: bad driver reference");
ThrowIfNULL(inIOCycleInfo, CAException(kAudioHardwareIllegalOperationError), "BGM_EndIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device, CAException(kAudioHardwareBadObjectError), "BGM_EndIOOperation: unknown device");
ThrowIf(inDriver != gAudioServerPlugInDriverRef,
CAException(kAudioHardwareBadObjectError),
"BGM_EndIOOperation: bad driver reference");
ThrowIfNULL(inIOCycleInfo,
CAException(kAudioHardwareIllegalOperationError),
"BGM_EndIOOperation: no cycle info");
ThrowIf(inDeviceObjectID != kObjectID_Device && inDeviceObjectID != kObjectID_Device_Null,
CAException(kAudioHardwareBadDeviceError),
"BGM_EndIOOperation: unknown device");
// tell the device to do the work
BGM_Device::GetInstance().EndIOOperation(inOperationID, inIOBufferFrameSize, *inIOCycleInfo, inClientID);
BGM_LookUpDevice(inDeviceObjectID).EndIOOperation(inOperationID,
inIOBufferFrameSize,
*inIOCycleInfo,
inClientID);
}
catch(const CAException& inException)
{
@ -805,6 +951,9 @@ static OSStatus BGM_EndIOOperation(AudioServerPlugInDriverRef inDriver, AudioObj
}
catch(...)
{
DebugMsg("BGM_PlugInInterface::BGM_EndIOOperation: unknown exception. (device: %s, operation: %u)",
(inDeviceObjectID == kObjectID_Device ? "BGMDevice" : "other"),
inOperationID);
theAnswer = kAudioHardwareUnspecifiedError;
}

View file

@ -0,0 +1,488 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
// BGM_Stream.cpp
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
// Portions copyright (C) 2013 Apple Inc. All Rights Reserved.
//
// Self Include
#include "BGM_Stream.h"
// Local Includes
#include "BGM_Types.h"
#include "BGM_Utils.h"
#include "BGM_Device.h"
#include "BGM_PlugIn.h"
// PublicUtility Includes
#include "CADebugMacros.h"
#include "CAException.h"
#include "CAPropertyAddress.h"
#include "CADispatchQueue.h"
#pragma clang assume_nonnull begin
BGM_Stream::BGM_Stream(AudioObjectID inObjectID,
AudioDeviceID inOwnerDeviceID,
bool inIsInput,
Float64 inSampleRate,
UInt32 inStartingChannel)
:
BGM_Object(inObjectID, kAudioStreamClassID, kAudioObjectClassID, inOwnerDeviceID),
mStateMutex(inIsInput ? "Input Stream State" : "Output Stream State"),
mIsInput(inIsInput),
mIsStreamActive(false),
mSampleRate(inSampleRate),
mStartingChannel(inStartingChannel)
{
}
BGM_Stream::~BGM_Stream()
{
}
bool BGM_Stream::HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required. There is more detailed commentary about each property in the
// GetPropertyData() method.
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioStreamPropertyIsActive:
case kAudioStreamPropertyDirection:
case kAudioStreamPropertyTerminalType:
case kAudioStreamPropertyStartingChannel:
case kAudioStreamPropertyLatency:
case kAudioStreamPropertyVirtualFormat:
case kAudioStreamPropertyPhysicalFormat:
case kAudioStreamPropertyAvailableVirtualFormats:
case kAudioStreamPropertyAvailablePhysicalFormats:
theAnswer = true;
break;
default:
theAnswer = BGM_Object::HasProperty(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
bool BGM_Stream::IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required. There is more detailed commentary about each property in the
// GetPropertyData() method.
bool theAnswer = false;
switch(inAddress.mSelector)
{
case kAudioStreamPropertyDirection:
case kAudioStreamPropertyTerminalType:
case kAudioStreamPropertyStartingChannel:
case kAudioStreamPropertyLatency:
case kAudioStreamPropertyAvailableVirtualFormats:
case kAudioStreamPropertyAvailablePhysicalFormats:
theAnswer = false;
break;
case kAudioStreamPropertyIsActive:
case kAudioStreamPropertyVirtualFormat:
case kAudioStreamPropertyPhysicalFormat:
theAnswer = true;
break;
default:
theAnswer = BGM_Object::IsPropertySettable(inObjectID, inClientPID, inAddress);
break;
};
return theAnswer;
}
UInt32 BGM_Stream::GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData) const
{
// For each object, this driver implements all the required properties plus a few extras that
// are useful but not required. There is more detailed commentary about each property in the
// GetPropertyData() method.
UInt32 theAnswer = 0;
switch(inAddress.mSelector)
{
case kAudioStreamPropertyIsActive:
theAnswer = sizeof(UInt32);
break;
case kAudioStreamPropertyDirection:
theAnswer = sizeof(UInt32);
break;
case kAudioStreamPropertyTerminalType:
theAnswer = sizeof(UInt32);
break;
case kAudioStreamPropertyStartingChannel:
theAnswer = sizeof(UInt32);
break;
case kAudioStreamPropertyLatency:
theAnswer = sizeof(UInt32);
break;
case kAudioStreamPropertyVirtualFormat:
case kAudioStreamPropertyPhysicalFormat:
theAnswer = sizeof(AudioStreamBasicDescription);
break;
case kAudioStreamPropertyAvailableVirtualFormats:
case kAudioStreamPropertyAvailablePhysicalFormats:
theAnswer = 1 * sizeof(AudioStreamRangedDescription);
break;
default:
theAnswer = BGM_Object::GetPropertyDataSize(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData);
break;
};
return theAnswer;
}
void BGM_Stream::GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const
{
// Since most of the data that will get returned is static, there are few instances where it is
// necessary to lock the state mutex.
switch(inAddress.mSelector)
{
case kAudioObjectPropertyBaseClass:
// The base class for kAudioStreamClassID is kAudioObjectClassID
ThrowIf(inDataSize < sizeof(AudioClassID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioObjectPropertyBaseClass for the stream");
*reinterpret_cast<AudioClassID*>(outData) = kAudioObjectClassID;
outDataSize = sizeof(AudioClassID);
break;
case kAudioObjectPropertyClass:
// Streams are of the class kAudioStreamClassID
ThrowIf(inDataSize < sizeof(AudioClassID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioObjectPropertyClass for the stream");
*reinterpret_cast<AudioClassID*>(outData) = kAudioStreamClassID;
outDataSize = sizeof(AudioClassID);
break;
case kAudioObjectPropertyOwner:
// A stream's owner is a device object.
{
ThrowIf(inDataSize < sizeof(AudioObjectID),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioObjectPropertyOwner for the stream");
// Lock the state mutex to create a memory barrier, just in case a subclass ever
// allows mOwnerObjectID to be modified.
CAMutex::Locker theStateLocker(mStateMutex);
// Return the requested value.
*reinterpret_cast<AudioObjectID*>(outData) = mOwnerObjectID;
outDataSize = sizeof(AudioObjectID);
}
break;
case kAudioStreamPropertyIsActive:
// This property tells the device whether or not the given stream is going to
// be used for IO. Note that we need to take the state lock to examine this
// value.
{
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioStreamPropertyIsActive for the stream");
// Take the state lock.
CAMutex::Locker theStateLocker(mStateMutex);
// Return the requested value.
*reinterpret_cast<UInt32*>(outData) = mIsStreamActive;
outDataSize = sizeof(UInt32);
}
break;
case kAudioStreamPropertyDirection:
// This returns whether the stream is an input or output stream.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return value of "
"kAudioStreamPropertyDirection for the stream");
*reinterpret_cast<UInt32*>(outData) = mIsInput ? 1 : 0;
outDataSize = sizeof(UInt32);
break;
case kAudioStreamPropertyTerminalType:
// This returns a value that indicates what is at the other end of the stream
// such as a speaker or headphones or a microphone.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return value of "
"kAudioStreamPropertyTerminalType for the stream");
*reinterpret_cast<UInt32*>(outData) =
mIsInput ? kAudioStreamTerminalTypeMicrophone : kAudioStreamTerminalTypeSpeaker;
outDataSize = sizeof(UInt32);
break;
case kAudioStreamPropertyStartingChannel:
// This property returns the absolute channel number for the first channel in
// the stream. For example, if a device has two output streams with two
// channels each, then the starting channel number for the first stream is 1
// and the starting channel number for the second stream is 3.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioStreamPropertyStartingChannel for the stream");
*reinterpret_cast<UInt32*>(outData) = mStartingChannel;
outDataSize = sizeof(UInt32);
break;
case kAudioStreamPropertyLatency:
// This property returns any additonal presentation latency the stream has.
ThrowIf(inDataSize < sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioStreamPropertyLatency for the stream");
*reinterpret_cast<UInt32*>(outData) = 0;
outDataSize = sizeof(UInt32);
break;
case kAudioStreamPropertyVirtualFormat:
case kAudioStreamPropertyPhysicalFormat:
// This returns the current format of the stream in an AudioStreamBasicDescription.
// For devices that don't override the mix operation, the virtual format has to be the
// same as the physical format.
{
ThrowIf(inDataSize < sizeof(AudioStreamBasicDescription),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::GetPropertyData: not enough space for the return "
"value of kAudioStreamPropertyVirtualFormat for the stream");
// This particular device always vends 32-bit native endian floats
AudioStreamBasicDescription* outASBD =
reinterpret_cast<AudioStreamBasicDescription*>(outData);
// Our streams have the same sample rate as the device they belong to.
outASBD->mSampleRate = mSampleRate;
outASBD->mFormatID = kAudioFormatLinearPCM;
outASBD->mFormatFlags =
kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
outASBD->mBytesPerPacket = 8;
outASBD->mFramesPerPacket = 1;
outASBD->mBytesPerFrame = 8;
outASBD->mChannelsPerFrame = 2;
outASBD->mBitsPerChannel = 32;
outDataSize = sizeof(AudioStreamBasicDescription);
}
break;
case kAudioStreamPropertyAvailableVirtualFormats:
case kAudioStreamPropertyAvailablePhysicalFormats:
// This returns an array of AudioStreamRangedDescriptions that describe what
// formats are supported.
if((inDataSize / sizeof(AudioStreamRangedDescription)) >= 1)
{
AudioStreamRangedDescription* outASRD =
reinterpret_cast<AudioStreamRangedDescription*>(outData);
outASRD[0].mFormat.mSampleRate = mSampleRate;
outASRD[0].mFormat.mFormatID = kAudioFormatLinearPCM;
outASRD[0].mFormat.mFormatFlags =
kAudioFormatFlagIsFloat | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked;
outASRD[0].mFormat.mBytesPerPacket = 8;
outASRD[0].mFormat.mFramesPerPacket = 1;
outASRD[0].mFormat.mBytesPerFrame = 8;
outASRD[0].mFormat.mChannelsPerFrame = 2;
outASRD[0].mFormat.mBitsPerChannel = 32;
// These match kAudioDevicePropertyAvailableNominalSampleRates.
outASRD[0].mSampleRateRange.mMinimum = 1.0;
outASRD[0].mSampleRateRange.mMaximum = 1000000000.0;
// Report how much we wrote.
outDataSize = sizeof(AudioStreamRangedDescription);
}
else
{
outDataSize = 0;
}
break;
default:
BGM_Object::GetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
outDataSize,
outData);
break;
};
}
void BGM_Stream::SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
const void* inData)
{
// There is more detailed commentary about each property in the GetPropertyData() method.
switch(inAddress.mSelector)
{
case kAudioStreamPropertyIsActive:
{
// Changing the active state of a stream doesn't affect IO or change the structure
// so we can just save the state and send the notification.
ThrowIf(inDataSize != sizeof(UInt32),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::SetPropertyData: wrong size for the data for "
"kAudioStreamPropertyIsActive");
bool theNewIsActive = *reinterpret_cast<const UInt32*>(inData) != 0;
CAMutex::Locker theStateLocker(mStateMutex);
if(mIsStreamActive != theNewIsActive)
{
mIsStreamActive = theNewIsActive;
// Send the notification.
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theProperty[] = {
CAPropertyAddress(kAudioStreamPropertyIsActive)
};
BGM_PlugIn::Host_PropertiesChanged(inObjectID, 1, theProperty);
});
}
}
break;
case kAudioStreamPropertyVirtualFormat:
case kAudioStreamPropertyPhysicalFormat:
{
// The device that owns the stream handles changing the stream format, as it needs
// to be handled via the RequestConfigChange/PerformConfigChange machinery. The
// stream only needs to validate the format at this point.
//
// Note that because our devices only supports 2 channel 32 bit float data, the only
// thing that can change is the sample rate.
ThrowIf(inDataSize != sizeof(AudioStreamBasicDescription),
CAException(kAudioHardwareBadPropertySizeError),
"BGM_Stream::SetPropertyData: wrong size for the data for "
"kAudioStreamPropertyPhysicalFormat");
const AudioStreamBasicDescription* theNewFormat =
reinterpret_cast<const AudioStreamBasicDescription*>(inData);
ThrowIf(theNewFormat->mFormatID != kAudioFormatLinearPCM,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported format ID for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mFormatFlags !=
(kAudioFormatFlagIsFloat |
kAudioFormatFlagsNativeEndian |
kAudioFormatFlagIsPacked),
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported format flags for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mBytesPerPacket != 8,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported bytes per packet for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mFramesPerPacket != 1,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported frames per packet for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mBytesPerFrame != 8,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported bytes per frame for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mChannelsPerFrame != 2,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported channels per frame for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mBitsPerChannel != 32,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported bits per channel for "
"kAudioStreamPropertyPhysicalFormat");
ThrowIf(theNewFormat->mSampleRate < 1.0,
CAException(kAudioDeviceUnsupportedFormatError),
"BGM_Stream::SetPropertyData: unsupported sample rate for "
"kAudioStreamPropertyPhysicalFormat");
}
break;
default:
BGM_Object::SetPropertyData(inObjectID,
inClientPID,
inAddress,
inQualifierDataSize,
inQualifierData,
inDataSize,
inData);
break;
};
}
#pragma mark Accessors
void BGM_Stream::SetSampleRate(Float64 inSampleRate)
{
CAMutex::Locker theStateLocker(mStateMutex);
mSampleRate = inSampleRate;
}
#pragma clang assume_nonnull end

View file

@ -0,0 +1,112 @@
// This file is part of Background Music.
//
// Background Music is free software: you can redistribute it and/or
// modify it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 2 of the
// License, or (at your option) any later version.
//
// Background Music is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Background Music. If not, see <http://www.gnu.org/licenses/>.
// BGM_Stream.h
// BGMDriver
//
// Copyright © 2017 Kyle Neideck
//
// An input or output audio stream. Each stream belongs to a device (see BGM_AbstractDevice), which
// in turn belongs to a plug-in (see BGM_PlugIn).
//
// This class only handles the stream's HAL properties, i.e. the metadata about the stream, not the
// audio data itself.
//
#ifndef BGMDriver__BGM_Stream
#define BGMDriver__BGM_Stream
// SuperClass Includes
#include "BGM_Object.h"
// PublicUtility Includes
#include "CAMutex.h"
// System Includes
#include <CoreAudio/AudioHardwareBase.h>
#pragma clang assume_nonnull begin
class BGM_Stream
:
public BGM_Object
{
#pragma mark Construction/Destruction
public:
BGM_Stream(AudioObjectID inObjectID,
AudioObjectID inOwnerDeviceID,
bool inIsInput,
Float64 inSampleRate,
UInt32 inStartingChannel = 1);
virtual ~BGM_Stream();
#pragma mark Property Operations
public:
bool HasProperty(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
bool IsPropertySettable(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress) const;
UInt32 GetPropertyDataSize(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData) const;
void GetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
UInt32& outDataSize,
void* outData) const;
void SetPropertyData(AudioObjectID inObjectID,
pid_t inClientPID,
const AudioObjectPropertyAddress& inAddress,
UInt32 inQualifierDataSize,
const void* __nullable inQualifierData,
UInt32 inDataSize,
const void* inData);
#pragma mark Accessors
void SetSampleRate(Float64 inSampleRate);
private:
CAMutex mStateMutex;
bool mIsInput;
Float64 mSampleRate;
/*! True if the stream is enabled and doing IO. See kAudioStreamPropertyIsActive. */
bool mIsStreamActive;
/*!
The absolute channel number for the first channel in the stream. For example, if a device has
two output streams with two channels each, then the starting channel number for the first
stream is 1 and the starting channel number for the second stream is 3. See
kAudioStreamPropertyStartingChannel.
*/
UInt32 mStartingChannel;
};
#pragma clang assume_nonnull end
#endif /* BGMDriver__BGM_Stream */

View file

@ -254,8 +254,6 @@ void BGM_ClientMap::CopyClientIntoAppVolumesArray(BGM_Client inClient, CAVolu
}
}
// TODO: Combine the SetClientsRelativeVolume methods? Their code is very similar.
template <typename T>
std::vector<BGM_Client*> * _Nullable GetClientsFromMap(std::map<T, std::vector<BGM_Client*>> & map, T key) {
auto theClientItr = map.find(key);
@ -279,7 +277,7 @@ void ShowSetRelativeVolumeMessage(CACFString inAppBundleID, BGM_Client* theClien
void ShowSetRelativeVolumeMessage(pid_t inAppPID, BGM_Client* theClient) {
(void)inAppPID;
(void)theClient;
DebugMsg("BGM_ClientMap::SetClientsRelativeVolume: Set volume %f for client %u by pid (%d)",
DebugMsg("BGM_ClientMap::ShowSetRelativeVolumeMessage: Set volume %f for client %u by pid (%d)",
theClient->mRelativeVolume,
theClient->mClientID,
inAppPID);
@ -288,7 +286,7 @@ void ShowSetRelativeVolumeMessage(pid_t inAppPID, BGM_Client* theClient) {
void ShowSetRelativeVolumeMessage(CACFString inAppBundleID, BGM_Client* theClient) {
(void)inAppBundleID;
(void)theClient;
DebugMsg("BGM_ClientMap::SetClientsRelativeVolume: Set volume %f for client %u by bundle ID (%s)",
DebugMsg("BGM_ClientMap::ShowSetRelativeVolumeMessage: Set volume %f for client %u by bundle ID (%s)",
theClient->mRelativeVolume,
theClient->mClientID,
CFStringGetCStringPtr(inAppBundleID.GetCFString(), kCFStringEncodingUTF8));
@ -380,9 +378,6 @@ bool BGM_ClientMap::SetClientsPanPosition(pid_t searchKey, SInt32 inPanPosition)
if(theClients != nullptr) {
for(auto theClient: *theClients) {
theClient->mPanPosition = inPanPosition;
// ShowSetPanPositionsMessage(searchKey, theClient)
didChangePanPosition = true;
}
}
@ -393,7 +388,6 @@ bool BGM_ClientMap::SetClientsPanPosition(pid_t searchKey, SInt32 inPanPosition)
theSetPansInShadowMapsFunc();
return didChangePanPosition;
}
bool BGM_ClientMap::SetClientsPanPosition(CACFString searchKey, SInt32 inPanPosition)
@ -408,9 +402,6 @@ bool BGM_ClientMap::SetClientsPanPosition(CACFString searchKey, SInt32 inPanPosi
if(theClients != nullptr) {
for(auto theClient: *theClients) {
theClient->mPanPosition = inPanPosition;
// ShowSetPanPositionsMessage(searchKey, theClient)
didChangePanPosition = true;
}
}
@ -421,7 +412,6 @@ bool BGM_ClientMap::SetClientsPanPosition(CACFString searchKey, SInt32 inPanPosi
theSetPansInShadowMapsFunc();
return didChangePanPosition;
}
void BGM_ClientMap::UpdateClientIOStateNonRT(UInt32 inClientID, bool inDoingIO)

View file

@ -31,6 +31,7 @@
// PublicUtility Includes
#include "CAException.h"
#include "CACFDictionary.h"
#include "CADispatchQueue.h"
#pragma mark Construction/Destruction
@ -209,24 +210,26 @@ void BGM_Clients::SendIORunningNotifications(bool sendIsRunningNotification,
{
if(sendIsRunningNotification || sendIsRunningSomewhereOtherThanBGMAppNotification)
{
AudioObjectPropertyAddress theChangedProperties[2];
UInt32 theNotificationCount = 0;
if(sendIsRunningNotification)
{
DebugMsg("BGM_Clients::SendIORunningNotifications: Sending kAudioDevicePropertyDeviceIsRunning");
theChangedProperties[0] = { kAudioDevicePropertyDeviceIsRunning, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster };
theNotificationCount++;
}
if(sendIsRunningSomewhereOtherThanBGMAppNotification)
{
DebugMsg("BGM_Clients::SendIORunningNotifications: Sending kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp");
theChangedProperties[theNotificationCount] = kBGMRunningSomewhereOtherThanBGMAppAddress;
theNotificationCount++;
}
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device, theNotificationCount, theChangedProperties);
CADispatchQueue::GetGlobalSerialQueue().Dispatch(false, ^{
AudioObjectPropertyAddress theChangedProperties[2];
UInt32 theNotificationCount = 0;
if(sendIsRunningNotification)
{
DebugMsg("BGM_Clients::SendIORunningNotifications: Sending kAudioDevicePropertyDeviceIsRunning");
theChangedProperties[0] = { kAudioDevicePropertyDeviceIsRunning, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster };
theNotificationCount++;
}
if(sendIsRunningSomewhereOtherThanBGMAppNotification)
{
DebugMsg("BGM_Clients::SendIORunningNotifications: Sending kAudioDeviceCustomPropertyDeviceIsRunningSomewhereOtherThanBGMApp");
theChangedProperties[theNotificationCount] = kBGMRunningSomewhereOtherThanBGMAppAddress;
theNotificationCount++;
}
BGM_PlugIn::Host_PropertiesChanged(kObjectID_Device, theNotificationCount, theChangedProperties);
});
}
}

View file

@ -0,0 +1,312 @@
/*
File: CAPropertyAddress.h
Abstract: Part of CoreAudio Utility Classes
Version: 1.1
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
Inc. ("Apple") in consideration of your agreement to the following
terms, and your use, installation, modification or redistribution of
this Apple software constitutes acceptance of these terms. If you do
not agree with these terms, please do not use, install, modify or
redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc. may
be used to endorse or promote products derived from the Apple Software
without specific prior written permission from Apple. Except as
expressly stated in this notice, no other rights or licenses, express or
implied, are granted by Apple herein, including but not limited to any
patent rights that may be infringed by your derivative works or by other
works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2014 Apple Inc. All Rights Reserved.
*/
#if !defined(__CAPropertyAddress_h__)
#define __CAPropertyAddress_h__
//==================================================================================================
// Includes
//==================================================================================================
// PublicUtility Includes
#include "CADebugMacros.h"
// System Includes
#include <CoreAudio/AudioHardware.h>
// Standard Library Includes
#include <algorithm>
#include <functional>
#include <vector>
//==================================================================================================
// CAPropertyAddress
//
// CAPropertyAddress extends the AudioObjectPropertyAddress structure to C++ including constructors
// and other utility operations. Note that there is no defined operator< or operator== because the
// presence of wildcards for the fields make comparisons ambiguous without specifying whether or
// not to take the wildcards into account. Consequently, if you want to use this struct in an STL
// data structure, you'll need to specify the approriate function object explicitly in the template
// declaration.
//==================================================================================================
struct CAPropertyAddress
:
public AudioObjectPropertyAddress
{
// Construction/Destruction
public:
CAPropertyAddress() : AudioObjectPropertyAddress() { mSelector = 0; mScope = kAudioObjectPropertyScopeGlobal; mElement = kAudioObjectPropertyElementMaster; }
CAPropertyAddress(AudioObjectPropertySelector inSelector) : AudioObjectPropertyAddress() { mSelector = inSelector; mScope = kAudioObjectPropertyScopeGlobal; mElement = kAudioObjectPropertyElementMaster; }
CAPropertyAddress(AudioObjectPropertySelector inSelector, AudioObjectPropertyScope inScope) : AudioObjectPropertyAddress() { mSelector = inSelector; mScope = inScope; mElement = kAudioObjectPropertyElementMaster; }
CAPropertyAddress(AudioObjectPropertySelector inSelector, AudioObjectPropertyScope inScope, AudioObjectPropertyElement inElement) : AudioObjectPropertyAddress() { mSelector = inSelector; mScope = inScope; mElement = inElement; }
CAPropertyAddress(const AudioObjectPropertyAddress& inAddress) : AudioObjectPropertyAddress(inAddress){}
CAPropertyAddress(const CAPropertyAddress& inAddress) : AudioObjectPropertyAddress(inAddress){}
CAPropertyAddress& operator=(const AudioObjectPropertyAddress& inAddress) { AudioObjectPropertyAddress::operator=(inAddress); return *this; }
CAPropertyAddress& operator=(const CAPropertyAddress& inAddress) { AudioObjectPropertyAddress::operator=(inAddress); return *this; }
// Operations
public:
static bool IsSameAddress(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) { return (inAddress1.mScope == inAddress2.mScope) && (inAddress1.mSelector == inAddress2.mSelector) && (inAddress1.mElement == inAddress2.mElement); }
static bool IsLessThanAddress(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) { bool theAnswer = false; if(inAddress1.mScope != inAddress2.mScope) { theAnswer = inAddress1.mScope < inAddress2.mScope; } else if(inAddress1.mSelector != inAddress2.mSelector) { theAnswer = inAddress1.mSelector < inAddress2.mSelector; } else { theAnswer = inAddress1.mElement < inAddress2.mElement; } return theAnswer; }
static bool IsCongruentSelector(AudioObjectPropertySelector inSelector1, AudioObjectPropertySelector inSelector2) { return (inSelector1 == inSelector2) || (inSelector1 == kAudioObjectPropertySelectorWildcard) || (inSelector2 == kAudioObjectPropertySelectorWildcard); }
static bool IsCongruentScope(AudioObjectPropertyScope inScope1, AudioObjectPropertyScope inScope2) { return (inScope1 == inScope2) || (inScope1 == kAudioObjectPropertyScopeWildcard) || (inScope2 == kAudioObjectPropertyScopeWildcard); }
static bool IsCongruentElement(AudioObjectPropertyElement inElement1, AudioObjectPropertyElement inElement2) { return (inElement1 == inElement2) || (inElement1 == kAudioObjectPropertyElementWildcard) || (inElement2 == kAudioObjectPropertyElementWildcard); }
static bool IsCongruentAddress(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) { return IsCongruentScope(inAddress1.mScope, inAddress2.mScope) && IsCongruentSelector(inAddress1.mSelector, inAddress2.mSelector) && IsCongruentElement(inAddress1.mElement, inAddress2.mElement); }
static bool IsCongruentLessThanAddress(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) { bool theAnswer = false; if(!IsCongruentScope(inAddress1.mScope, inAddress2.mScope)) { theAnswer = inAddress1.mScope < inAddress2.mScope; } else if(!IsCongruentSelector(inAddress1.mSelector, inAddress2.mSelector)) { theAnswer = inAddress1.mSelector < inAddress2.mSelector; } else if(!IsCongruentElement(inAddress1.mElement, inAddress2.mElement)) { theAnswer = inAddress1.mElement < inAddress2.mElement; } return theAnswer; }
// STL Helpers
public:
struct EqualTo : public std::binary_function<AudioObjectPropertyAddress, AudioObjectPropertyAddress, bool>
{
bool operator()(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) const { return IsSameAddress(inAddress1, inAddress2); }
};
struct LessThan : public std::binary_function<AudioObjectPropertyAddress, AudioObjectPropertyAddress, bool>
{
bool operator()(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) const { return IsLessThanAddress(inAddress1, inAddress2); }
};
struct CongruentEqualTo : public std::binary_function<AudioObjectPropertyAddress, AudioObjectPropertyAddress, bool>
{
bool operator()(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) const { return IsCongruentAddress(inAddress1, inAddress2); }
};
struct CongruentLessThan : public std::binary_function<AudioObjectPropertyAddress, AudioObjectPropertyAddress, bool>
{
bool operator()(const AudioObjectPropertyAddress& inAddress1, const AudioObjectPropertyAddress& inAddress2) const { return IsCongruentLessThanAddress(inAddress1, inAddress2); }
};
};
//==================================================================================================
// CAPropertyAddressList
//
// An auto-resizing array of CAPropertyAddress structures.
//==================================================================================================
class CAPropertyAddressList
{
// Construction/Destruction
public:
CAPropertyAddressList() : mAddressList(), mToken(NULL) {}
explicit CAPropertyAddressList(void* inToken) : mAddressList(), mToken(inToken) {}
explicit CAPropertyAddressList(uintptr_t inToken) : mAddressList(), mToken(reinterpret_cast<void*>(inToken)) {}
CAPropertyAddressList(const CAPropertyAddressList& inAddressList) : mAddressList(inAddressList.mAddressList), mToken(inAddressList.mToken) {}
CAPropertyAddressList& operator=(const CAPropertyAddressList& inAddressList) { mAddressList = inAddressList.mAddressList; mToken = inAddressList.mToken; return *this; }
~CAPropertyAddressList() {}
// Operations
public:
void* GetToken() const { return mToken; }
void SetToken(void* inToken) { mToken = inToken; }
uintptr_t GetIntToken() const { return reinterpret_cast<uintptr_t>(mToken); }
void SetIntToken(uintptr_t inToken) { mToken = reinterpret_cast<void*>(inToken); }
AudioObjectID GetAudioObjectIDToken() const { return static_cast<AudioObjectID>(reinterpret_cast<uintptr_t>(mToken)); }
bool IsEmpty() const { return mAddressList.empty(); }
UInt32 GetNumberItems() const { return ToUInt32(mAddressList.size()); }
void GetItemByIndex(UInt32 inIndex, AudioObjectPropertyAddress& outAddress) const { if(inIndex < mAddressList.size()) { outAddress = mAddressList.at(inIndex); } }
const AudioObjectPropertyAddress* GetItems() const { return &(*mAddressList.begin()); }
AudioObjectPropertyAddress* GetItems() { return &(*mAddressList.begin()); }
bool HasItem(const AudioObjectPropertyAddress& inAddress) const { AddressList::const_iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::CongruentEqualTo(), inAddress)); return theIterator != mAddressList.end(); }
bool HasExactItem(const AudioObjectPropertyAddress& inAddress) const { AddressList::const_iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::EqualTo(), inAddress)); return theIterator != mAddressList.end(); }
void AppendItem(const AudioObjectPropertyAddress& inAddress) { mAddressList.push_back(inAddress); }
void AppendUniqueItem(const AudioObjectPropertyAddress& inAddress) { if(!HasItem(inAddress)) { mAddressList.push_back(inAddress); } }
void AppendUniqueExactItem(const AudioObjectPropertyAddress& inAddress) { if(!HasExactItem(inAddress)) { mAddressList.push_back(inAddress); } }
void InsertItemAtIndex(UInt32 inIndex, const AudioObjectPropertyAddress& inAddress) { if(inIndex < mAddressList.size()) { AddressList::iterator theIterator = mAddressList.begin(); std::advance(theIterator, static_cast<int>(inIndex)); mAddressList.insert(theIterator, inAddress); } else { mAddressList.push_back(inAddress); } }
void EraseExactItem(const AudioObjectPropertyAddress& inAddress) { AddressList::iterator theIterator = std::find_if(mAddressList.begin(), mAddressList.end(), std::bind1st(CAPropertyAddress::EqualTo(), inAddress)); if(theIterator != mAddressList.end()) { mAddressList.erase(theIterator); } }
void EraseItemAtIndex(UInt32 inIndex) { if(inIndex < mAddressList.size()) { AddressList::iterator theIterator = mAddressList.begin(); std::advance(theIterator, static_cast<int>(inIndex)); mAddressList.erase(theIterator); } }
void EraseAllItems() { mAddressList.clear(); }
// Implementation
private:
typedef std::vector<CAPropertyAddress> AddressList;
AddressList mAddressList;
void* mToken;
};
//==================================================================================================
// CAPropertyAddressListVector
//
// An auto-resizing array of CAPropertyAddressList objects.
//==================================================================================================
class CAPropertyAddressListVector
{
// Construction/Destruction
public:
CAPropertyAddressListVector() : mAddressListVector() {}
CAPropertyAddressListVector(const CAPropertyAddressListVector& inAddressListVector) : mAddressListVector(inAddressListVector.mAddressListVector) {}
CAPropertyAddressListVector& operator=(const CAPropertyAddressListVector& inAddressListVector) { mAddressListVector = inAddressListVector.mAddressListVector; return *this; }
~CAPropertyAddressListVector() {}
// Operations
public:
bool IsEmpty() const { return mAddressListVector.empty(); }
bool HasAnyNonEmptyItems() const;
bool HasAnyItemsWithAddress(const AudioObjectPropertyAddress& inAddress) const;
bool HasAnyItemsWithExactAddress(const AudioObjectPropertyAddress& inAddress) const;
UInt32 GetNumberItems() const { return ToUInt32(mAddressListVector.size()); }
const CAPropertyAddressList& GetItemByIndex(UInt32 inIndex) const { return mAddressListVector.at(inIndex); }
CAPropertyAddressList& GetItemByIndex(UInt32 inIndex) { return mAddressListVector.at(inIndex); }
const CAPropertyAddressList* GetItemByToken(void* inToken) const;
CAPropertyAddressList* GetItemByToken(void* inToken);
const CAPropertyAddressList* GetItemByIntToken(uintptr_t inToken) const;
CAPropertyAddressList* GetItemByIntToken(uintptr_t inToken);
void AppendItem(const CAPropertyAddressList& inAddressList) { mAddressListVector.push_back(inAddressList); }
void EraseAllItems() { mAddressListVector.clear(); }
// Implementation
private:
typedef std::vector<CAPropertyAddressList> AddressListVector;
AddressListVector mAddressListVector;
};
inline bool CAPropertyAddressListVector::HasAnyNonEmptyItems() const
{
bool theAnswer = false;
for(AddressListVector::const_iterator theIterator = mAddressListVector.begin(); !theAnswer && (theIterator != mAddressListVector.end()); ++theIterator)
{
theAnswer = !theIterator->IsEmpty();
}
return theAnswer;
}
inline bool CAPropertyAddressListVector::HasAnyItemsWithAddress(const AudioObjectPropertyAddress& inAddress) const
{
bool theAnswer = false;
for(AddressListVector::const_iterator theIterator = mAddressListVector.begin(); !theAnswer && (theIterator != mAddressListVector.end()); ++theIterator)
{
theAnswer = theIterator->HasItem(inAddress);
}
return theAnswer;
}
inline bool CAPropertyAddressListVector::HasAnyItemsWithExactAddress(const AudioObjectPropertyAddress& inAddress) const
{
bool theAnswer = false;
for(AddressListVector::const_iterator theIterator = mAddressListVector.begin(); !theAnswer && (theIterator != mAddressListVector.end()); ++theIterator)
{
theAnswer = theIterator->HasExactItem(inAddress);
}
return theAnswer;
}
inline const CAPropertyAddressList* CAPropertyAddressListVector::GetItemByToken(void* inToken) const
{
const CAPropertyAddressList* theAnswer = NULL;
bool wasFound = false;
for(AddressListVector::const_iterator theIterator = mAddressListVector.begin(); !wasFound && (theIterator != mAddressListVector.end()); ++theIterator)
{
if(theIterator->GetToken() == inToken)
{
wasFound = true;
theAnswer = &(*theIterator);
}
}
return theAnswer;
}
inline CAPropertyAddressList* CAPropertyAddressListVector::GetItemByToken(void* inToken)
{
CAPropertyAddressList* theAnswer = NULL;
bool wasFound = false;
for(AddressListVector::iterator theIterator = mAddressListVector.begin(); !wasFound && (theIterator != mAddressListVector.end()); ++theIterator)
{
if(theIterator->GetToken() == inToken)
{
wasFound = true;
theAnswer = &(*theIterator);
}
}
return theAnswer;
}
inline const CAPropertyAddressList* CAPropertyAddressListVector::GetItemByIntToken(uintptr_t inToken) const
{
const CAPropertyAddressList* theAnswer = NULL;
bool wasFound = false;
for(AddressListVector::const_iterator theIterator = mAddressListVector.begin(); !wasFound && (theIterator != mAddressListVector.end()); ++theIterator)
{
if(theIterator->GetIntToken() == inToken)
{
wasFound = true;
theAnswer = &(*theIterator);
}
}
return theAnswer;
}
inline CAPropertyAddressList* CAPropertyAddressListVector::GetItemByIntToken(uintptr_t inToken)
{
CAPropertyAddressList* theAnswer = NULL;
bool wasFound = false;
for(AddressListVector::iterator theIterator = mAddressListVector.begin(); !wasFound && (theIterator != mAddressListVector.end()); ++theIterator)
{
if(theIterator->GetIntToken() == inToken)
{
wasFound = true;
theAnswer = &(*theIterator);
}
}
return theAnswer;
}
#endif

View file

@ -36,7 +36,7 @@ No dependencies so far. (Though you're welcome to add some.)
The best place to start is probably [DEVELOPING.md](/DEVELOPING.md), which has an overview of the project and
instructions for building, debugging, etc. It's kind of long, though, and not very interesting, so you might prefer to
go straight into the code. In that case, you'll probably want to start with
[AppDelegate.mm](/BGMApp/BGMApp/AppDelegate.mm).
[BGMAppDelegate.mm](/BGMApp/BGMApp/BGMAppDelegate.mm).
If you get stuck or have questions about the project, feel free to open an issue. You could also [email
me](mailto:kyle@bearisdriving.com) or try [#backgroundmusic on

View file

@ -152,8 +152,8 @@ for Xcode package, which you can find in the [Apple developer downloads](https:/
BGMApp is a fairly standard Cocoa status-bar app, for the most part. The UI is simple and mostly built in Interface
Builder.
`awakeFromNib` in [AppDelegate.mm](/BGMApp/BGMApp/AppDelegate.mm) is (more or less) the entry point/main function.
`applicationDidFinishLaunching` gets called next and finishes setting things up.
`awakeFromNib` in [BGMAppDelegate.mm](/BGMApp/BGMApp/BGMAppDelegate.mm) is (more or less) the entry point/main
function. `applicationDidFinishLaunching` gets called next and finishes setting things up.
At launch, BGMApp sets BGMDevice as the system's default device and starts playing the audio from BGMDevice through the
actual output device. Usually that's the device that BGMDevice replaced when we set it as the default device. When

View file

@ -17,7 +17,7 @@
// BGM_Types.h
// SharedSource
//
// Copyright © 2016 Kyle Neideck
// Copyright © 2016, 2017 Kyle Neideck
//
#ifndef SharedSource__BGM_Types
@ -40,6 +40,8 @@ static const char* const kBGMIssueTrackerURL = "https://github.com/kyleneideck/B
#define kBGMDeviceUID "BGMDevice"
#define kBGMDeviceModelUID "BGMDeviceModelUID"
#define kBGMNullDeviceUID "BGMNullDevice"
#define kBGMNullDeviceModelUID "BGMNullDeviceModelUID"
// The object IDs for the audio objects this driver implements.
//
@ -48,11 +50,21 @@ static const char* const kBGMIssueTrackerURL = "https://github.com/kyleneideck/B
enum
{
kObjectID_PlugIn = kAudioObjectPlugInObject,
kObjectID_Device = 2,
kObjectID_Stream_Input = 3,
kObjectID_Stream_Output = 4,
kObjectID_Volume_Output_Master = 5,
kObjectID_Mute_Output_Master = 6
kObjectID_Device = 2, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Input = 3, // Belongs to kObjectID_Device
kObjectID_Stream_Output = 4, // Belongs to kObjectID_Device
kObjectID_Volume_Output_Master = 5, // Belongs to kObjectID_Device
kObjectID_Mute_Output_Master = 6, // Belongs to kObjectID_Device
kObjectID_Device_Null = 7, // Belongs to kObjectID_PlugIn
kObjectID_Stream_Null = 8, // Belongs to kObjectID_Device_Null
};
#pragma BGM Plug-in Custom Properties
enum
{
// A CFBoolean. True if the null device is enabled. Settable, false by default.
kAudioPlugInCustomPropertyNullDeviceActive = 'nuld'
};
#pragma mark BGMDevice Custom Properties
@ -85,7 +97,10 @@ enum
// Getting this property will only return apps with volumes other than the default. Setting this property
// will add new app volumes or replace existing ones, but there's currently no way to delete an app from
// the internal collection.
kAudioDeviceCustomPropertyAppVolumes = 'apvs'
kAudioDeviceCustomPropertyAppVolumes = 'apvs',
// A CFArray of CFBooleans indicating which of BGMDevice's controls are enabled. All controls are enabled
// by default. This property is settable. See the array indices below for more info.
kAudioDeviceCustomPropertyEnabledOutputControls = 'bgct'
};
// The number of silent/audible frames before BGMDriver will change kAudioDeviceCustomPropertyDeviceAudibleState
@ -129,6 +144,15 @@ enum
#define kAppPanCenterRawValue 0
#define kAppPanRightRawValue 100
// kAudioDeviceCustomPropertyEnabledOutputControls indices
enum
{
// True if BGMDevice's master output volume control is enabled.
kBGMEnabledOutputControlsIndex_Volume = 0,
// True if BGMDevice's master output mute control is enabled.
kBGMEnabledOutputControlsIndex_Mute = 1
};
#pragma mark BGMDevice Custom Property Addresses
// For convenience.
@ -163,6 +187,12 @@ static const AudioObjectPropertyAddress kBGMAppVolumesAddress = {
kAudioObjectPropertyElementMaster
};
static const AudioObjectPropertyAddress kBGMEnabledOutputControlsAddress = {
kAudioDeviceCustomPropertyEnabledOutputControls,
kAudioObjectPropertyScopeOutput,
kAudioObjectPropertyElementMaster
};
#pragma mark XPC Return Codes
enum {