#audioDucking.py #A part of NonVisual Desktop Access (NVDA) #Copyright (C) 2015-2016 NV Access Limited #This file is covered by the GNU General Public License. #See the file COPYING for more details. import threading from ctypes import * import time import config from logHandler import log def _isDebug(): return config.conf["debugLog"]["audioDucking"] class AutoEvent(wintypes.HANDLE): def __init__(self): e=windll.kernel32.CreateEventW(None,True,False,None) super(AutoEvent,self).__init__(e) def __del__(self): if self: windll.kernel32.CloseHandle(self) WAIT_TIMEOUT=0x102 AUDIODUCKINGMODE_NONE=0 AUDIODUCKINGMODE_OUTPUTTING=1 AUDIODUCKINGMODE_ALWAYS=2 audioDuckingModes=[ # Translators: An audio ducking mode which specifies how NVDA affects the volume of other applications. # See the Audio Ducking Mode section of the User Guide for details. _("No ducking"), # Translators: An audio ducking mode which specifies how NVDA affects the volume of other applications. # See the Audio Ducking Mode section of the User Guide for details. _("Duck when outputting speech and sounds"), # Translators: An audio ducking mode which specifies how NVDA affects the volume of other applications. # See the Audio Ducking Mode section of the User Guide for details. _("Always duck"), ] ANRUS_ducking_AUDIO_ACTIVE=4 ANRUS_ducking_AUDIO_ACTIVE_NODUCK=8 INITIAL_DUCKING_DELAY=0.15 _audioDuckingMode=0 _duckingRefCount=0 _duckingRefCountLock = threading.RLock() _modeChangeEvent=None _lastDuckedTime=0 def _setDuckingState(switch): global _lastDuckedTime with _duckingRefCountLock: import gui ATWindow=gui.mainFrame.GetHandle() if switch: oledll.oleacc.AccSetRunningUtilityState(ATWindow,ANRUS_ducking_AUDIO_ACTIVE|ANRUS_ducking_AUDIO_ACTIVE_NODUCK,ANRUS_ducking_AUDIO_ACTIVE|ANRUS_ducking_AUDIO_ACTIVE_NODUCK) _lastDuckedTime=time.time() else: oledll.oleacc.AccSetRunningUtilityState(ATWindow,ANRUS_ducking_AUDIO_ACTIVE|ANRUS_ducking_AUDIO_ACTIVE_NODUCK,ANRUS_ducking_AUDIO_ACTIVE_NODUCK) def _ensureDucked(): global _duckingRefCount with _duckingRefCountLock: _duckingRefCount+=1 if _isDebug(): log.debug("Increased ref count, _duckingRefCount=%d"%_duckingRefCount) if _duckingRefCount==1 and _audioDuckingMode!=AUDIODUCKINGMODE_NONE: _setDuckingState(True) delta=0 else: delta=time.time()-_lastDuckedTime return delta,_modeChangeEvent def _unensureDucked(delay=True): global _duckingRefCount if delay: import core if _isDebug(): log.debug("Queuing _unensureDucked") core.callLater(1000,_unensureDucked,False) return with _duckingRefCountLock: _duckingRefCount-=1 if _isDebug(): log.debug("Decreased ref count, _duckingRefCount=%d"%_duckingRefCount) if _duckingRefCount==0 and _audioDuckingMode!=AUDIODUCKINGMODE_NONE: _setDuckingState(False) def setAudioDuckingMode(mode): global _audioDuckingMode, _modeChangeEvent if not isAudioDuckingSupported(): raise RuntimeError("audio ducking not supported") if mode<0 or mode>=len(audioDuckingModes): raise ValueError("%s is not an audio ducking mode") with _duckingRefCountLock: oldMode=_audioDuckingMode _audioDuckingMode=mode if _modeChangeEvent: windll.kernel32.SetEvent(_modeChangeEvent) _modeChangeEvent=AutoEvent() if _isDebug(): log.debug("Switched modes from %s, to %s"%(oldMode,mode)) if oldMode==AUDIODUCKINGMODE_NONE and mode!=AUDIODUCKINGMODE_NONE and _duckingRefCount>0: _setDuckingState(True) elif oldMode!=AUDIODUCKINGMODE_NONE and mode==AUDIODUCKINGMODE_NONE and _duckingRefCount>0: _setDuckingState(False) if oldMode!=AUDIODUCKINGMODE_ALWAYS and mode==AUDIODUCKINGMODE_ALWAYS: _ensureDucked() elif oldMode==AUDIODUCKINGMODE_ALWAYS and mode!=AUDIODUCKINGMODE_ALWAYS: _unensureDucked(delay=False) def initialize(): if not isAudioDuckingSupported(): return _setDuckingState(False) setAudioDuckingMode(config.conf['audio']['audioDuckingMode']) _isAudioDuckingSupported=None def isAudioDuckingSupported(): global _isAudioDuckingSupported if _isAudioDuckingSupported is None: _isAudioDuckingSupported=config.isInstalledCopy() and hasattr(oledll.oleacc,'AccSetRunningUtilityState') return _isAudioDuckingSupported def handleConfigProfileSwitch(): if isAudioDuckingSupported(): setAudioDuckingMode(config.conf['audio']['audioDuckingMode']) class AudioDucker(object): """ Create one of these objects to manage ducking of background audio. Use the enable and disable methods on this object to denote when you require audio to be ducked. If this object is deleted while ducking is still enabled, the object will automatically disable ducking first. """ def __init__(self): if not isAudioDuckingSupported(): raise RuntimeError("audio ducking not supported") self._enabled=False self._lock=threading.Lock() def __del__(self): if self._enabled: self.disable() def enable(self): """Tells NVDA that you require that background audio be ducked from now until you call disable. This method may block for a short time while background audio ducks to a suitable level. It is safe to call this method more than once. @ returns: True if ducking was enabled, false if ducking was subsiquently disabled while waiting for the background audio to drop. """ debug = _isDebug() with self._lock: if self._enabled: if debug: log.debug("ignoring duplicate enable") return True self._enabled=True if debug: log.debug("enabling") whenWasDucked,modeChangeEvent=_ensureDucked() deltaMS=int((INITIAL_DUCKING_DELAY-whenWasDucked)*1000) disableEvent=self._disabledEvent=AutoEvent() if debug: log.debug("whenWasDucked %s, deltaMS %s"%(whenWasDucked,deltaMS)) if deltaMS<=0 or _audioDuckingMode==AUDIODUCKINGMODE_NONE: return True import NVDAHelper if not NVDAHelper.localLib.audioDucking_shouldDelay(): if debug: log.debug("No background audio, not delaying") return True if debug: log.debug("waiting %s ms or mode change"%deltaMS) wasCanceled=windll.kernel32.WaitForMultipleObjects(2,(wintypes.HANDLE*2)(disableEvent,modeChangeEvent),False,deltaMS)!=WAIT_TIMEOUT if debug: log.debug("Wait canceled" if wasCanceled else "timeout exceeded") return not wasCanceled def disable(self): """Tells NVDA that you no longer require audio to be ducked. while other AudioDucker objects are still enabled, audio will remain ducked. It is safe to call this method more than once. """ with self._lock: if not self._enabled: if _isDebug(): log.debug("Ignoring duplicate disable") return True self._enabled=False if _isDebug(): log.debug("disabling") _unensureDucked() windll.kernel32.SetEvent(self._disabledEvent) return True