forked from nvaccess/nvda
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathaudioDucking.py
252 lines (214 loc) · 8.31 KB
/
audioDucking.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2015-2021 NV Access Limited
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
from enum import IntEnum
from utils.displayString import DisplayStringIntEnum
import threading
from typing import Dict
from ctypes import oledll, wintypes, windll
import time
import config
from logHandler import log
import systemUtils
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
class AudioDuckingMode(DisplayStringIntEnum):
NONE = 0
OUTPUTTING = 1
ALWAYS = 2
@property
def _displayStringLabels(self) -> Dict[IntEnum, str]:
return {
# 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.
AudioDuckingMode.NONE: _("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.
AudioDuckingMode.OUTPUTTING: _("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.
AudioDuckingMode.ALWAYS: _("Always duck"),
}
class ANRUSDucking(IntEnum):
# https://docs.microsoft.com/en-us/windows/win32/api/oleacc/nf-oleacc-accsetrunningutilitystate#anrus_priority_audio_active_noduck
AUDIO_ACTIVE = 4
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:
try:
import gui
ATWindow=gui.mainFrame.GetHandle()
if switch:
oledll.oleacc.AccSetRunningUtilityState(
ATWindow,
ANRUSDucking.AUDIO_ACTIVE | ANRUSDucking.AUDIO_ACTIVE_NODUCK,
ANRUSDucking.AUDIO_ACTIVE | ANRUSDucking.AUDIO_ACTIVE_NODUCK
)
_lastDuckedTime=time.time()
else:
oledll.oleacc.AccSetRunningUtilityState(
ATWindow,
ANRUSDucking.AUDIO_ACTIVE | ANRUSDucking.AUDIO_ACTIVE_NODUCK,
ANRUSDucking.AUDIO_ACTIVE_NODUCK
)
except WindowsError as e:
# When the NVDA build is not signed, audio ducking fails with access denied.
# A developer built launcher is unlikely to be signed. Catching this error stops developers from looking into
# "expected" errors.
# ERROR_ACCESS_DENIED is 0x5
# https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--0-499-
ERROR_ACCESS_DENIED = 0x80070005
errorCode = e.winerror & 0xFFFFFFFF # we only care about the first 8 hex values.
if errorCode == ERROR_ACCESS_DENIED:
log.warning("Unable to set ducking state: ERROR_ACCESS_DENIED.")
else:
# we want developers to hear the "error sound", and to halt, so still raise the exception.
log.error(
"Unknown error when setting ducking state: Error number: {:#010X}".format(errorCode),
exc_info=True
)
raise e
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(AudioDuckingMode):
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'])
config.post_configProfileSwitch.register(handlePostConfigProfileSwitch)
_isAudioDuckingSupported=None
def isAudioDuckingSupported():
global _isAudioDuckingSupported
if _isAudioDuckingSupported is None:
_isAudioDuckingSupported = (
config.isInstalledCopy()
or config.isAppX
) and hasattr(oledll.oleacc, 'AccSetRunningUtilityState')
_isAudioDuckingSupported &= systemUtils.hasUiAccess()
return _isAudioDuckingSupported
def handlePostConfigProfileSwitch():
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