Skip to content

Commit b0228fe

Browse files
Temporarily suspend audio ducking when a 32 bit synthDriver is in use (#19665)
Partial fix for #19618 ### Summary of the issue: As 32 bit sapi synthDrivers introduced in pr #19432 produce audio directly in their own process, NVDA currently cannot correctly duck audio when they are in use. Specifically, if NVDA is set to always duck, the audio from these synths is ducked along with other external audio. And if set to duck for speech and sounds, their audio does not cause ducking, and any NvDA sound that does, ducks their audio. The correct approach to fix this for the long-term is to broker all 32 bit audio through NVDA, rather than it being played directly by the external process. See pr #19577. But until then, we should at least consider tempoarily disabling audio ducking while one of these synthDrivers is in use, so that its audio is not inappropriately ducked. ### Description of development approach: * Added a new private `_AudioDuckingSuspender` class to `audioDucking` which when at least one instance exists, temporarily suspends audio ducking, and disallows changing the current audio ducking setting via the gesture or GUI setting. When all instances are deleted, then audio ducking is restored back to the state it was before one or more instances were created. * `_bridge`'s `SynthDriverProxy` class when instantiated now creates an instance of `_AudioDuckingsuspender` and holds it on the SynthDriverProxy instance, thus causing audio ducking to be temporarily disabled while this synthDriver is in use. ### Testing strategy: With a copy of NVDA that supports audio ducking: * Using eSpeak, set audio ducking via the gesture to always duck. Confirm that audio stays ducked. * Choose the sapi 32 bit synth from the Select Synthesizer dialog. Confirm that audio is no longer ducked. * Try to cycle through audio ducking modes with `NVDA+shift+d`. Confirm that NvDA reports that audio ducking is not supported. * Go to the audio pannel in the NvDA settings dialog. Confirm that the Audio ducking mode control is disabled. * Choose eSpeak from the Selected synthesizer dialog. Confirm that audio is again ducked. * Try to cycle through the audio ducking modes with the gesture. Confirm that this works. * Confirm that the audio ducking mode in the Audio panel of the NvDA settings dialog is no longer disabled. ### Known issues with pull request: * This is a temporary partial fix that just ensures that audio ducking is correctly disabled. The full fix is to broker audio and again fully support audio ducking. PR #19432. * An alternative would be to grant the 32 bit synthDriver runtime process UIAccess and build audio ducking directly into it. However, adding a second process with UIAccess would greatly increase our possible attack surface, and is not moving in the direction of a secure add-on runtime.
1 parent 3291f42 commit b0228fe

4 files changed

Lines changed: 55 additions & 2 deletions

File tree

source/_bridge/components/proxies/synthDriver.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import weakref
99
import typing
1010
from collections import OrderedDict
11+
import audioDucking
1112
from logHandler import log
1213
from _bridge.base import Proxy
1314
from autoSettingsUtils.driverSetting import DriverSetting, NumericDriverSetting, BooleanDriverSetting
@@ -38,9 +39,17 @@
3839
class SynthDriverProxy(Proxy, SynthDriver):
3940
"""Wraps a remote SynthDriverService, providing the same interface as a local SynthDriver."""
4041

42+
_audioDuckingSuspender: audioDucking._AudioDuckingSuspender | None = None
43+
4144
def __init__(self, service: SynthDriverService):
4245
log.debug(f"Creating SynthDriverProxy instance for remote synth driver '{self.name}'")
4346
super().__init__(service)
47+
if audioDucking.isAudioDuckingSupported():
48+
# Proxied synthDrivers cannot currently support audio ducking because they produce audio directly
49+
# in their own process, and NVDA cannot correctly duck this external audio. Therefore, we create
50+
# an _AudioDuckingSuspender to ensure that audio ducking is suspended while any proxied synth
51+
# driver exists.
52+
self._audioDuckingSuspender = audioDucking._AudioDuckingSuspender()
4453
selfRef = weakref.ref(self)
4554
for notification in self.supportedNotifications:
4655
if notification is synthIndexReached:

source/audioDucking.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,3 +266,47 @@ def disable(self):
266266
_unensureDucked()
267267
winBindings.kernel32.SetEvent(self._disabledEvent)
268268
return True
269+
270+
271+
_audioDuckingSuspenderRefCount: int = 0
272+
_audioDuckingSuspenderLock = threading.Lock()
273+
274+
275+
class _AudioDuckingSuspender: # pyright: ignore[reportUnusedClass]
276+
"""Create one of these objects to temporarily suspend audio ducking.
277+
If this object is deleted and no other _AudioDuckingSuspender objects exist, audio ducking will be re-enabled.
278+
"""
279+
280+
def __init__(self):
281+
if not isAudioDuckingSupported():
282+
raise RuntimeError("audio ducking not supported")
283+
global _audioDuckingSuspenderRefCount
284+
with _audioDuckingSuspenderLock:
285+
if _audioDuckingSuspenderRefCount == 0:
286+
setAudioDuckingMode(AudioDuckingMode.NONE)
287+
_audioDuckingSuspenderRefCount += 1
288+
if _isDebug():
289+
log.debug(f"Audio ducking suspended, count={_audioDuckingSuspenderRefCount}")
290+
291+
def __del__(self):
292+
global _audioDuckingSuspenderRefCount
293+
with _audioDuckingSuspenderLock:
294+
_audioDuckingSuspenderRefCount -= 1
295+
if _isDebug():
296+
log.debug(
297+
f"Audio ducking suspender ref count decreased, count={_audioDuckingSuspenderRefCount}",
298+
)
299+
if _audioDuckingSuspenderRefCount == 0:
300+
try:
301+
setAudioDuckingMode(config.conf["audio"]["audioDuckingMode"])
302+
except Exception:
303+
# Avoid raising from __del__; just log the error in debug builds.
304+
if _isDebug():
305+
log.exception(
306+
"Failed to restore audio ducking mode during _AudioDuckingSuspender cleanup",
307+
)
308+
309+
310+
def _isAudioDuckingSuspended() -> bool:
311+
with _audioDuckingSuspenderLock:
312+
return _audioDuckingSuspenderRefCount > 0

source/globalCommands.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ class GlobalCommands(ScriptableObject):
192192
gesture="kb:NVDA+shift+d",
193193
)
194194
def script_cycleAudioDuckingMode(self, gesture):
195-
if not audioDucking.isAudioDuckingSupported():
195+
if not audioDucking.isAudioDuckingSupported() or audioDucking._isAudioDuckingSuspended():
196196
# Translators: a message when audio ducking is not supported on this machine
197197
ui.message(_("Audio ducking not supported"))
198198
return

source/gui/settingsDialogs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3530,7 +3530,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer) -> None:
35303530
self.bindHelpEvent("SelectSynthesizerDuckingMode", self.duckingList)
35313531
index = config.conf["audio"]["audioDuckingMode"]
35323532
self.duckingList.SetSelection(index)
3533-
if not audioDucking.isAudioDuckingSupported():
3533+
if not audioDucking.isAudioDuckingSupported() or audioDucking._isAudioDuckingSuspended():
35343534
self.duckingList.Disable()
35353535

35363536
# Translators: This is the label for a checkbox control in the

0 commit comments

Comments
 (0)