From df45ca7c7c62fb979aea743275f69a9443ca92d4 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:57:25 +1100 Subject: [PATCH 01/25] Initial implementation of translation between mmDevice endpoint ID strings and WaveOut device IDs. --- source/synthDrivers/_sapi4.py | 23 ++++++++++++++--- source/synthDrivers/sapi4.py | 48 +++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 5d6c1586643..3d2e0f96e95 100755 --- a/source/synthDrivers/_sapi4.py +++ b/source/synthDrivers/_sapi4.py @@ -1,7 +1,5 @@ -# _sapi4.py -# Contributed by Serotek Corporation under the GPL # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2008 NVDA Contributors +# Copyright (C) 2006-2025 NV Access Limited, Serotek Corporation # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -18,6 +16,7 @@ POINTER, sizeof, Structure, + windll, ) from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD from comtypes import GUID, IUnknown, STDMETHOD @@ -227,3 +226,21 @@ class ITTSNotifySinkW(IUnknown): CLSID_MMAudioDest = GUID("{CB96B400-C743-11cd-80E5-00AA003E4B50}") CLSID_TTSEnumerator = GUID("{D67C0280-C743-11cd-80E5-00AA003E4B50}") + + +# WaveOutMessage message codes +# Defined in mmddk.h +DRV_QUERYFUNCTIONINSTANCEID = 2065 +DRV_QUERYFUNCTIONINSTANCEIDSIZE = 2066 +# Defined in mmsyscom.h +MMSYSERR_NOERROR = 0 + + +winmm = windll.winmm +waveOutMessage = winmm.waveOutMessage +# waveOutMessage.argtypes = [HANDLE, c_ulong, DWORD, DWORD] +# waveOutMessage.restype = c_uint + +waveOutGetNumDevs = winmm.waveOutGetNumDevs +# waveOutGetNumDevs.argtypes = [] +# waveOutGetNumDevs.restype = c_int diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index ea28eae3ed9..0c33bf31ba1 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -1,18 +1,19 @@ # A part of NonVisual Desktop Access (NVDA) # Copyright (C) 2006-2024 NV Access Limited, Leonard de Ruijter # This file is covered by the GNU General Public License. -# See the file COPYING for more details. +# See the file COPYING for Not e details. import locale from collections import OrderedDict import winreg from comtypes import CoCreateInstance, COMObject, COMError, GUID -from ctypes import byref, c_ulong, POINTER -from ctypes.wintypes import DWORD, WORD +from ctypes import byref, c_ulong, POINTER, c_wchar, create_string_buffer, sizeof +from ctypes.wintypes import DWORD, HANDLE, WORD from typing import Optional from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking from logHandler import log from ._sapi4 import ( + MMSYSERR_NOERROR, CLSID_MMAudioDest, CLSID_TTSEnumerator, IAudioMultiMediaDevice, @@ -33,9 +34,12 @@ TTSFEATURE_VOLUME, TTSMODEINFO, VOICECHARSET, + waveOutGetNumDevs, + waveOutMessage, + DRV_QUERYFUNCTIONINSTANCEID, + DRV_QUERYFUNCTIONINSTANCEIDSIZE, ) import config -import nvwave import weakref from speech.commands import ( @@ -233,7 +237,7 @@ def _set_voice(self, val): raise ValueError("no such mode: %s" % val) self._currentMode = mode self._ttsAudio = CoCreateInstance(CLSID_MMAudioDest, IAudioMultiMediaDevice) - self._ttsAudio.DeviceNumSet(nvwave.outputDeviceNameToID(config.conf["audio"]["outputDevice"], True)) + self._ttsAudio.DeviceNumSet(_mmDeviceEndpointIdToWaveOutId(config.conf["speech"]["outputDevice"])) self._ttsCentral = POINTER(ITTSCentralW)() self._ttsEngines.Select(self._currentMode.gModeID, byref(self._ttsCentral), self._ttsAudio) self._ttsAttrs = self._ttsCentral.QueryInterface(ITTSAttributes) @@ -365,3 +369,37 @@ def _set_volume(self, val: int): # using the low word for the left channel and the high word for the right channel. val |= val << 16 self._ttsAttrs.VolumeSet(val) + + +def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: + if targetEndpointId != config.conf.getConfigValidation(("audio", "outputDevice")).default: + targetEndpointIdByteCount = (len(targetEndpointId) + 1) * sizeof(c_wchar) + currEndpointId = create_string_buffer(targetEndpointIdByteCount) + currEndpointIdByteCount = DWORD() + for devID in range(waveOutGetNumDevs()): + # Get the length of this device's endpoint ID string. + mmr = waveOutMessage( + HANDLE(devID), + DRV_QUERYFUNCTIONINSTANCEIDSIZE, + byref(currEndpointIdByteCount), + None, + ) + if (mmr != MMSYSERR_NOERROR) or (currEndpointIdByteCount.value != targetEndpointIdByteCount): + # ID lengths don't match, so this device can't be a match. + continue + # Get the device's endpoint ID string. + mmr = waveOutMessage( + HANDLE(devID), + DRV_QUERYFUNCTIONINSTANCEID, + byref(currEndpointId), + currEndpointIdByteCount, + ) + if mmr != MMSYSERR_NOERROR: + continue + # Decode the endpoint ID string to a python string, and strip the null terminator. + if ( + currEndpointId.raw[: targetEndpointIdByteCount - sizeof(c_wchar)].decode("utf-16") + == targetEndpointId + ): + return devID + return -1 From 16b377d220b7cb2b9ec51dae780b02418f87aa52 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:13:16 +1100 Subject: [PATCH 02/25] Slight improvements to typing --- source/synthDrivers/_sapi4.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 3d2e0f96e95..9f0426c9982 100755 --- a/source/synthDrivers/_sapi4.py +++ b/source/synthDrivers/_sapi4.py @@ -235,12 +235,12 @@ class ITTSNotifySinkW(IUnknown): # Defined in mmsyscom.h MMSYSERR_NOERROR = 0 - +# Function prototypes +# Defined in mmeapi.h winmm = windll.winmm waveOutMessage = winmm.waveOutMessage -# waveOutMessage.argtypes = [HANDLE, c_ulong, DWORD, DWORD] -# waveOutMessage.restype = c_uint +waveOutMessage.restype = c_uint waveOutGetNumDevs = winmm.waveOutGetNumDevs -# waveOutGetNumDevs.argtypes = [] -# waveOutGetNumDevs.restype = c_int +waveOutGetNumDevs.argtypes = [] +waveOutGetNumDevs.restype = c_int From 3e2794bb6cfe5f0c4b594007c043b7c69abc3060 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:23:35 +1100 Subject: [PATCH 03/25] Fix incorrect config path --- source/synthDrivers/sapi4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 0c33bf31ba1..0961ad5e086 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -237,7 +237,7 @@ def _set_voice(self, val): raise ValueError("no such mode: %s" % val) self._currentMode = mode self._ttsAudio = CoCreateInstance(CLSID_MMAudioDest, IAudioMultiMediaDevice) - self._ttsAudio.DeviceNumSet(_mmDeviceEndpointIdToWaveOutId(config.conf["speech"]["outputDevice"])) + self._ttsAudio.DeviceNumSet(_mmDeviceEndpointIdToWaveOutId(config.conf["audio"]["outputDevice"])) self._ttsCentral = POINTER(ITTSCentralW)() self._ttsEngines.Select(self._currentMode.gModeID, byref(self._ttsCentral), self._ttsAudio) self._ttsAttrs = self._ttsCentral.QueryInterface(ITTSAttributes) From 6bf3f01c81cc9f155c06a411203d32037703cb65 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:30:59 +1100 Subject: [PATCH 04/25] Added a warning when SAPI4 is in use. --- source/config/configSpec.py | 1 + source/speech/speech.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 8a6a082dab1..8923793469f 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -45,6 +45,7 @@ autoDialectSwitching = boolean(default=false) delayedCharacterDescriptions = boolean(default=false) excludedSpeechModes = int_list(default=list()) + hasSapi4WarningBeenShown = boolean(default=False) [[__many__]] capPitchChange = integer(default=30,min=-100,max=100) diff --git a/source/speech/speech.py b/source/speech/speech.py index 49992c0b03f..dbf52d489aa 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -11,14 +11,17 @@ import weakref import unicodedata import time + import colors import api from annotation import _AnnotationRolesT import controlTypes from controlTypes import OutputReason, TextPosition from controlTypes.state import State +from gui.message import MessageDialog +import queueHandler import tones -from synthDriverHandler import getSynth +from synthDriverHandler import SynthDriver, getSynth, synthChanged import re import textInfos import speechDictHandler @@ -3059,3 +3062,34 @@ def clearTypedWordBuffer() -> None: complete the word (such as a focus change or choosing to move the caret). """ _curWordChars.clear() + + +def _sapi4DeprecationWarning(synth: SynthDriver, audioOutputDevice: str, isFallback: bool): + """A synthChanged event handler to alert the user about the deprecation of SAPI4.""" + + def setShown(): + config.conf["speech"]["hasSapi4WarningBeenShown"] = True + + def impl(): + MessageDialog( + parent=None, + message="Microsoft Speech API version 4 is obsolete. " + "Using this speech synthesizer may pose a security risk. " + "This synthesizer driver will be removed in NVDA 2026.1. " + "You are strongly encouraged to choose a more modern speech synthesizer.", + title="Warning", + buttons=None, + ).addOkButton( + callback=setShown, + ).Show() + + if ( + (not isFallback) + and (synth.name == "sapi4") + and (not config.conf["speech"]["hasSapi4WarningBeenShown"]) + ): + # We need to queue the dialog to appear, as wx may not have been initialised the first time this is called. + queueHandler.queueFunction(queueHandler.eventQueue, impl) + + +synthChanged.register(_sapi4DeprecationWarning) From c7d363bfd59d86da0235914c326c4d1231ae3dac Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:47:44 +1100 Subject: [PATCH 05/25] Changelog --- user_docs/en/changes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index e7bbe509074..c62d81ff729 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -4,6 +4,9 @@ ### Important notes +* Users are warned that support for Microsoft Speech API version 4 synthesizers is planned for removal in NVDA 2026.1. +Any remaining users of SAPI4 speech synthesizers are encouraged to choose a more modern speech synthesizer. + ### New Features * Support for math in PDFs has been added. @@ -179,6 +182,7 @@ Use `gui.message.MessageDialog` instead. (#17582) * `NoConsoleOptionParser`, `stringToBool`, `stringToLang` in `__main__`; use the same symbols in `argsParsing` instead. * `__main__.parser`; use `argsParsing.getParser()` instead. * `bdDetect.DeviceType` is deprecated in favour of `bdDetect.ProtocolType` and `bdDetect.CommunicationType` to take into account the fact that both HID and Serial communication can take place over USB and Bluetooth. (#17537 , @LeonarddeR) +* SAPI4, and all of its code, are deprecated, and planned for removal in 2026.1. ## 2024.4.2 From 6da072fa8957b8fc4025d4c790ba8c6173baa19b Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:20:22 +1100 Subject: [PATCH 06/25] Update copyright headers --- source/config/configSpec.py | 2 +- source/speech/speech.py | 2 +- source/synthDrivers/sapi4.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 8923793469f..ac49ddf072e 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2024 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt, +# Copyright (C) 2006-2025 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt, # Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith, # Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka # This file is covered by the GNU General Public License. diff --git a/source/speech/speech.py b/source/speech/speech.py index dbf52d489aa..3afb2b0780b 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -1,7 +1,7 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2006-2024 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, +# Copyright (C) 2006-2025 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, # Julien Cochuyt, Derek Riemer, Cyrille Bougot, Leonard de Ruijter, Łukasz Golonka """High-level functions to speak information.""" diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 0961ad5e086..434802fc3b4 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -1,7 +1,7 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2024 NV Access Limited, Leonard de Ruijter +# Copyright (C) 2006-2025 NV Access Limited, Leonard de Ruijter # This file is covered by the GNU General Public License. -# See the file COPYING for Not e details. +# See the file COPYING for more details. import locale from collections import OrderedDict From d0781baf7549fea6466e7a10484571d899d1f883 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:24:08 +1100 Subject: [PATCH 07/25] Update changes --- user_docs/en/changes.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index c62d81ff729..0e02fef7791 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -4,8 +4,8 @@ ### Important notes -* Users are warned that support for Microsoft Speech API version 4 synthesizers is planned for removal in NVDA 2026.1. -Any remaining users of SAPI4 speech synthesizers are encouraged to choose a more modern speech synthesizer. +* The support for Microsoft Speech API version 4 synthesizers is planned for removal in NVDA 2026.1. +Any remaining users of SAPI4 speech synthesizers are encouraged to choose a more modern speech synthesizer. (#17599) ### New Features @@ -182,7 +182,7 @@ Use `gui.message.MessageDialog` instead. (#17582) * `NoConsoleOptionParser`, `stringToBool`, `stringToLang` in `__main__`; use the same symbols in `argsParsing` instead. * `__main__.parser`; use `argsParsing.getParser()` instead. * `bdDetect.DeviceType` is deprecated in favour of `bdDetect.ProtocolType` and `bdDetect.CommunicationType` to take into account the fact that both HID and Serial communication can take place over USB and Bluetooth. (#17537 , @LeonarddeR) -* SAPI4, and all of its code, are deprecated, and planned for removal in 2026.1. +* SAPI4, `synthDrivers.sapi4`, is deprecated and planned for removal in 2026.1. (#17599) ## 2024.4.2 From 2af45cf26150c5fbeb58af4bdf25c20cd947aa6f Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:44:35 +1100 Subject: [PATCH 08/25] Switch to a driver message enum --- source/synthDrivers/_sapi4.py | 17 +++++++++++++---- source/synthDrivers/sapi4.py | 7 +++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 9f0426c9982..79910290492 100755 --- a/source/synthDrivers/_sapi4.py +++ b/source/synthDrivers/_sapi4.py @@ -19,6 +19,7 @@ windll, ) from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD +from enum import IntEnum from comtypes import GUID, IUnknown, STDMETHOD import winKernel @@ -228,10 +229,18 @@ class ITTSNotifySinkW(IUnknown): CLSID_TTSEnumerator = GUID("{D67C0280-C743-11cd-80E5-00AA003E4B50}") -# WaveOutMessage message codes -# Defined in mmddk.h -DRV_QUERYFUNCTIONINSTANCEID = 2065 -DRV_QUERYFUNCTIONINSTANCEIDSIZE = 2066 +class DriverMessage(IntEnum): + """WaveOutMessage message codes + Defined in mmddk.h + """ + + QUERY__INSTANCE_ID = 2065 + """DRV_QUERYFUNCTIONINSTANCEID """ + + QUERY_INSTANCE_ID_SIZE = 2066 + """DRV_QUERYFUNCTIONINSTANCEIDSIZE """ + + # Defined in mmsyscom.h MMSYSERR_NOERROR = 0 diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 434802fc3b4..5d93dd18d05 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -36,8 +36,7 @@ VOICECHARSET, waveOutGetNumDevs, waveOutMessage, - DRV_QUERYFUNCTIONINSTANCEID, - DRV_QUERYFUNCTIONINSTANCEIDSIZE, + DriverMessage, ) import config import weakref @@ -380,7 +379,7 @@ def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: # Get the length of this device's endpoint ID string. mmr = waveOutMessage( HANDLE(devID), - DRV_QUERYFUNCTIONINSTANCEIDSIZE, + DriverMessage.QUERY_INSTANCE_ID_SIZE, byref(currEndpointIdByteCount), None, ) @@ -390,7 +389,7 @@ def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: # Get the device's endpoint ID string. mmr = waveOutMessage( HANDLE(devID), - DRV_QUERYFUNCTIONINSTANCEID, + DriverMessage.QUERY__INSTANCE_ID, byref(currEndpointId), currEndpointIdByteCount, ) From ef063040c60698f1717a045144f93d9a8cee8110 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:49:13 +1100 Subject: [PATCH 09/25] Move loading winmm into _mmDeviceEndpointIdToWaveOutId --- source/synthDrivers/_sapi4.py | 19 ++++--------------- source/synthDrivers/sapi4.py | 9 ++++++--- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 79910290492..260cdd92782 100755 --- a/source/synthDrivers/_sapi4.py +++ b/source/synthDrivers/_sapi4.py @@ -16,7 +16,6 @@ POINTER, sizeof, Structure, - windll, ) from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD from enum import IntEnum @@ -229,6 +228,10 @@ class ITTSNotifySinkW(IUnknown): CLSID_TTSEnumerator = GUID("{D67C0280-C743-11cd-80E5-00AA003E4B50}") +# Defined in mmsyscom.h +MMSYSERR_NOERROR = 0 + + class DriverMessage(IntEnum): """WaveOutMessage message codes Defined in mmddk.h @@ -239,17 +242,3 @@ class DriverMessage(IntEnum): QUERY_INSTANCE_ID_SIZE = 2066 """DRV_QUERYFUNCTIONINSTANCEIDSIZE """ - - -# Defined in mmsyscom.h -MMSYSERR_NOERROR = 0 - -# Function prototypes -# Defined in mmeapi.h -winmm = windll.winmm -waveOutMessage = winmm.waveOutMessage -waveOutMessage.restype = c_uint - -waveOutGetNumDevs = winmm.waveOutGetNumDevs -waveOutGetNumDevs.argtypes = [] -waveOutGetNumDevs.restype = c_int diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 5d93dd18d05..6ef239f83de 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -7,7 +7,7 @@ from collections import OrderedDict import winreg from comtypes import CoCreateInstance, COMObject, COMError, GUID -from ctypes import byref, c_ulong, POINTER, c_wchar, create_string_buffer, sizeof +from ctypes import byref, c_ulong, POINTER, c_wchar, create_string_buffer, sizeof, windll from ctypes.wintypes import DWORD, HANDLE, WORD from typing import Optional from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking @@ -34,8 +34,6 @@ TTSFEATURE_VOLUME, TTSMODEINFO, VOICECHARSET, - waveOutGetNumDevs, - waveOutMessage, DriverMessage, ) import config @@ -375,6 +373,11 @@ def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: targetEndpointIdByteCount = (len(targetEndpointId) + 1) * sizeof(c_wchar) currEndpointId = create_string_buffer(targetEndpointIdByteCount) currEndpointIdByteCount = DWORD() + # Function prototypes + # Defined in mmeapi.h + winmm = windll.winmm + waveOutMessage = winmm.waveOutMessage + waveOutGetNumDevs = winmm.waveOutGetNumDevs for devID in range(waveOutGetNumDevs()): # Get the length of this device's endpoint ID string. mmr = waveOutMessage( From 8c7007a8a8dc3cd10765f9bd4e9ab3756fede881 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:11:35 +1100 Subject: [PATCH 10/25] Documentation improvements --- source/synthDrivers/sapi4.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 6ef239f83de..acbc2e2a5ec 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -369,11 +369,16 @@ def _set_volume(self, val: int): def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: + """Translate from an MMDevice Endpoint ID string to a WaveOut Device ID number. + + :param targetEndpointId: MMDevice endpoint ID string to translate from, or the default value of the `audio.outputDevice` configuration key for the default output device. + :return: An integer WaveOut device ID for use with SAPI4. + If no matching device is found, or the default output device is requested, `-1` is returned, which means output will be handled by Microsoft Sound Mapper. + """ if targetEndpointId != config.conf.getConfigValidation(("audio", "outputDevice")).default: targetEndpointIdByteCount = (len(targetEndpointId) + 1) * sizeof(c_wchar) currEndpointId = create_string_buffer(targetEndpointIdByteCount) currEndpointIdByteCount = DWORD() - # Function prototypes # Defined in mmeapi.h winmm = windll.winmm waveOutMessage = winmm.waveOutMessage @@ -404,4 +409,6 @@ def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: == targetEndpointId ): return devID + # No matching device found, or default requested explicitly. + # Return the ID of Microsoft Sound Mapper return -1 From 48b4d6eff78bd66909c3d4a0c93735e4bdaa5d63 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:17:04 +1100 Subject: [PATCH 11/25] Add deprecation warning --- source/synthDrivers/sapi4.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index acbc2e2a5ec..aebc4c69c95 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -2,6 +2,7 @@ # Copyright (C) 2006-2025 NV Access Limited, Leonard de Ruijter # This file is covered by the GNU General Public License. # See the file COPYING for more details. +# This module is deprecated, pending removal in NVDA 2026.1. import locale from collections import OrderedDict @@ -12,6 +13,7 @@ from typing import Optional from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking from logHandler import log +import warnings from ._sapi4 import ( MMSYSERR_NOERROR, CLSID_MMAudioDest, @@ -52,6 +54,9 @@ from speech.types import SpeechSequence +warnings.warn("synthDrivers.sapi4 is deprecated, pending removal in NVDA 2026.1.", DeprecationWarning) + + class SynthDriverBufSink(COMObject): _com_interfaces_ = [ITTSBufNotifySink] From 181b36a10d4b484d9566363f4826b33440e182b0 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:20:20 +1100 Subject: [PATCH 12/25] Added note about SAPI4's deprecation to the UG --- user_docs/en/userGuide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index 8b5ef0d340b..1025a6bb853 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -3883,6 +3883,7 @@ There are also many variants which can be chosen to alter the sound of the voice SAPI 4 is an older Microsoft standard for software speech synthesizers. NVDA still supports this for users who already have SAPI 4 synthesizers installed. However, Microsoft no longer support this and needed components are no longer available from Microsoft. +Support for SAPI4 will be removed in NVDA 2026.1. When using this synthesizer with NVDA, the available voices (accessed from the [Speech category](#SpeechSettings) of the [NVDA Settings](#NVDASettings) dialog or by the [Synth Settings Ring](#SynthSettingsRing)) will contain all the voices from all the installed SAPI 4 engines found on your system. From d27751377af306c45b853806b089818a6e2ac198 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:17:03 +1100 Subject: [PATCH 13/25] Only show warning when not minimal --- source/speech/speech.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source/speech/speech.py b/source/speech/speech.py index 3afb2b0780b..1511bef026c 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -18,6 +18,7 @@ import controlTypes from controlTypes import OutputReason, TextPosition from controlTypes.state import State +import globalVars from gui.message import MessageDialog import queueHandler import tones @@ -3092,4 +3093,7 @@ def impl(): queueHandler.queueFunction(queueHandler.eventQueue, impl) -synthChanged.register(_sapi4DeprecationWarning) +if not globalVars.appArgs.minimal: + # Don't warn users about SAPI4 deprecation in minimal mode. + # This stops the dialog appearing on secure screens or in the launcher. + synthChanged.register(_sapi4DeprecationWarning) From 306bd86d9958103c77762210ec6d9f124af7c2a9 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:22:54 +1100 Subject: [PATCH 14/25] Mark hasSapi4WarningBeenShown as private --- source/config/configSpec.py | 2 +- source/speech/speech.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index ac49ddf072e..ca844060b72 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -45,7 +45,7 @@ autoDialectSwitching = boolean(default=false) delayedCharacterDescriptions = boolean(default=false) excludedSpeechModes = int_list(default=list()) - hasSapi4WarningBeenShown = boolean(default=False) + _hasSapi4WarningBeenShown = boolean(default=False) [[__many__]] capPitchChange = integer(default=30,min=-100,max=100) diff --git a/source/speech/speech.py b/source/speech/speech.py index 1511bef026c..75eed8e70aa 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -3069,7 +3069,7 @@ def _sapi4DeprecationWarning(synth: SynthDriver, audioOutputDevice: str, isFallb """A synthChanged event handler to alert the user about the deprecation of SAPI4.""" def setShown(): - config.conf["speech"]["hasSapi4WarningBeenShown"] = True + config.conf["speech"]["_hasSapi4WarningBeenShown"] = True def impl(): MessageDialog( @@ -3087,7 +3087,7 @@ def impl(): if ( (not isFallback) and (synth.name == "sapi4") - and (not config.conf["speech"]["hasSapi4WarningBeenShown"]) + and (not config.conf["speech"]["_hasSapi4WarningBeenShown"]) ): # We need to queue the dialog to appear, as wx may not have been initialised the first time this is called. queueHandler.queueFunction(queueHandler.eventQueue, impl) From b3bf4035c05e8bf24a33b150b7c66dc75dc1475d Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:11:34 +1100 Subject: [PATCH 15/25] Make SAPI4 warning translatable --- source/speech/speech.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/source/speech/speech.py b/source/speech/speech.py index 75eed8e70aa..2a5c63a9f2c 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -3074,11 +3074,15 @@ def setShown(): def impl(): MessageDialog( parent=None, - message="Microsoft Speech API version 4 is obsolete. " - "Using this speech synthesizer may pose a security risk. " - "This synthesizer driver will be removed in NVDA 2026.1. " - "You are strongly encouraged to choose a more modern speech synthesizer.", - title="Warning", + message=_( + # Translators: Message warning users that SAPI4 is deprecated. + "Microsoft Speech API version 4 is obsolete. " + "Using this speech synthesizer may pose a security risk. " + "This synthesizer driver will be removed in NVDA 2026.1. " + "You are strongly encouraged to choose a more modern speech synthesizer.", + ), + # Translators: Title of a message dialog. + title=_("Warning"), buttons=None, ).addOkButton( callback=setShown, From 3b9c2bcfd8cc7f6722a380a2e24b598b5cee1a00 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:58:12 +1100 Subject: [PATCH 16/25] Don't show DriverSettings with ids that start with a _ in the GUI --- source/autoSettingsUtils/driverSetting.py | 1 + source/gui/settingsDialogs.py | 3 +++ user_docs/en/changes.md | 1 + 3 files changed, 5 insertions(+) diff --git a/source/autoSettingsUtils/driverSetting.py b/source/autoSettingsUtils/driverSetting.py index be8f3dbb09e..08a4c7f2d4d 100644 --- a/source/autoSettingsUtils/driverSetting.py +++ b/source/autoSettingsUtils/driverSetting.py @@ -49,6 +49,7 @@ def __init__( ): """ @param id: internal identifier of the setting + If this starts with a `_`, it will not be shown in the settings GUI. @param displayNameWithAccelerator: the localized string shown in voice or braille settings dialog @param availableInSettingsRing: Will this option be available in a settings ring? @param defaultVal: Specifies the default value for a driver setting. diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 793f6e04f32..d9c9540bd7a 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1552,6 +1552,9 @@ def updateDriverSettings(self, changedSetting=None): continue if setting.id in self.sizerDict: # update a value self._updateValueForControl(setting, settingsStorage) + elif setting.id.startswith("_"): + # Skip private settings. + continue else: # create a new control self._createNewControl(setting, settingsStorage) # Update graphical layout of the dialog diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 8c507908cbf..0828ab897b7 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -182,6 +182,7 @@ Instead, a `callback` property has been added, which returns a function that per * Because SAPI5 voices now use `nvwave.WavePlayer` to output audio: (#17592, @gexgd0419) * `synthDrivers.sapi5.SPAudioState` has been removed. * `synthDrivers.sapi5.SynthDriver.ttsAudioStream` has been removed. +* Instances of `autoSettingsUtils.driverSetting.DriverSetting` with an `id` that starts with an underscore (_) are no longer shown in NVDA's settings. (#17599) #### Deprecations From 04f9a78fdbf47d8e0b67e8c1bd960739263e882f Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 16 Jan 2025 17:59:27 +1100 Subject: [PATCH 17/25] Switch to using a SAPI4 setting for the warning --- source/config/configSpec.py | 1 - source/speech/speech.py | 9 +++------ source/synthDrivers/sapi4.py | 6 +++++- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index ca844060b72..d7fd524ce8e 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -45,7 +45,6 @@ autoDialectSwitching = boolean(default=false) delayedCharacterDescriptions = boolean(default=false) excludedSpeechModes = int_list(default=list()) - _hasSapi4WarningBeenShown = boolean(default=False) [[__many__]] capPitchChange = integer(default=30,min=-100,max=100) diff --git a/source/speech/speech.py b/source/speech/speech.py index 2a5c63a9f2c..be9b0f6ec32 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -3069,7 +3069,8 @@ def _sapi4DeprecationWarning(synth: SynthDriver, audioOutputDevice: str, isFallb """A synthChanged event handler to alert the user about the deprecation of SAPI4.""" def setShown(): - config.conf["speech"]["_hasSapi4WarningBeenShown"] = True + setattr(synth, "_hasWarningBeenShown", True) + synth.saveSettings() def impl(): MessageDialog( @@ -3088,11 +3089,7 @@ def impl(): callback=setShown, ).Show() - if ( - (not isFallback) - and (synth.name == "sapi4") - and (not config.conf["speech"]["_hasSapi4WarningBeenShown"]) - ): + if (not isFallback) and (synth.name == "sapi4") and (not getattr(synth, "_hasWarningBeenShown", False)): # We need to queue the dialog to appear, as wx may not have been initialised the first time this is called. queueHandler.queueFunction(queueHandler.eventQueue, impl) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 44508784e69..8aa4a2a7eb8 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -11,6 +11,7 @@ from ctypes import byref, c_ulong, POINTER, c_wchar, create_string_buffer, sizeof, windll from ctypes.wintypes import DWORD, HANDLE, WORD from typing import Optional +from autoSettingsUtils.driverSetting import BooleanDriverSetting from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking from logHandler import log import warnings @@ -128,7 +129,10 @@ def ITTSNotifySinkW_AudioStop(self, this, qTimeStamp: int): class SynthDriver(SynthDriver): name = "sapi4" description = "Microsoft Speech API version 4" - supportedSettings = [SynthDriver.VoiceSetting()] + supportedSettings = [ + SynthDriver.VoiceSetting(), + BooleanDriverSetting("_hasWarningBeenShown", ""), + ] supportedCommands = { IndexCommand, CharacterModeCommand, From f08a4874c9868f56c2ec121cc5298a8f1f95e201 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:05:48 +1100 Subject: [PATCH 18/25] Fixed accidental double underscore --- source/synthDrivers/_sapi4.py | 2 +- source/synthDrivers/sapi4.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 260cdd92782..5480b8d144f 100755 --- a/source/synthDrivers/_sapi4.py +++ b/source/synthDrivers/_sapi4.py @@ -237,7 +237,7 @@ class DriverMessage(IntEnum): Defined in mmddk.h """ - QUERY__INSTANCE_ID = 2065 + QUERY_INSTANCE_ID = 2065 """DRV_QUERYFUNCTIONINSTANCEID """ QUERY_INSTANCE_ID_SIZE = 2066 diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 8aa4a2a7eb8..2521685666c 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -462,7 +462,7 @@ def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: # Get the device's endpoint ID string. mmr = waveOutMessage( HANDLE(devID), - DriverMessage.QUERY__INSTANCE_ID, + DriverMessage.QUERY_INSTANCE_ID, byref(currEndpointId), currEndpointIdByteCount, ) From 53a2d7110477aac9310bdcd7af19f670fd9997e9 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Thu, 16 Jan 2025 18:17:24 +1100 Subject: [PATCH 19/25] Change to using secure instead of minimal --- source/speech/speech.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/speech/speech.py b/source/speech/speech.py index be9b0f6ec32..d9218ba971f 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -3094,7 +3094,7 @@ def impl(): queueHandler.queueFunction(queueHandler.eventQueue, impl) -if not globalVars.appArgs.minimal: - # Don't warn users about SAPI4 deprecation in minimal mode. - # This stops the dialog appearing on secure screens or in the launcher. +if not globalVars.appArgs.secure: + # Don't warn users about SAPI4 deprecation in secure mode. + # This stops the dialog appearing on secure screens and when secure mode has been forced. synthChanged.register(_sapi4DeprecationWarning) From 41774e9c5bf605d16436b90e33f153dc83b0e565 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:42:38 +1100 Subject: [PATCH 20/25] Set _hasWarningBeenShown directly --- source/speech/speech.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/speech/speech.py b/source/speech/speech.py index d9218ba971f..00cfddda3df 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -3069,7 +3069,7 @@ def _sapi4DeprecationWarning(synth: SynthDriver, audioOutputDevice: str, isFallb """A synthChanged event handler to alert the user about the deprecation of SAPI4.""" def setShown(): - setattr(synth, "_hasWarningBeenShown", True) + synth._hasWarningBeenShown = True synth.saveSettings() def impl(): From 6331ee12743e2576fa95cf917dd5b7bf84b83470 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:32:22 +1100 Subject: [PATCH 21/25] Add an "Open user guide" button and more explanatory text to the SAPI4 deprecation dialog. --- source/speech/speech.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/speech/speech.py b/source/speech/speech.py index 00cfddda3df..8d1032af0e5 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -20,6 +20,7 @@ from controlTypes.state import State import globalVars from gui.message import MessageDialog +import gui.contextHelp import queueHandler import tones from synthDriverHandler import SynthDriver, getSynth, synthChanged @@ -3080,13 +3081,18 @@ def impl(): "Microsoft Speech API version 4 is obsolete. " "Using this speech synthesizer may pose a security risk. " "This synthesizer driver will be removed in NVDA 2026.1. " - "You are strongly encouraged to choose a more modern speech synthesizer.", + "You are strongly encouraged to choose a more modern speech synthesizer. " + "Consult the Supported Speech Synthesizers section in the user guide for suggestions.", ), # Translators: Title of a message dialog. title=_("Warning"), buttons=None, ).addOkButton( callback=setShown, + ).addHelpButton( + # Translators: A button in a dialog. + label=_("Open user guide"), + callback=lambda: gui.contextHelp.showHelp("SupportedSpeechSynths"), ).Show() if (not isFallback) and (synth.name == "sapi4") and (not getattr(synth, "_hasWarningBeenShown", False)): From 1e2ae40a7c490c4c5990fd54aab059822f712b3e Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:33:55 +1100 Subject: [PATCH 22/25] Moved the sapi4 deprecation warning code to synthDrivers.sapi4. --- source/speech/speech.py | 46 +----------------------------------- source/synthDrivers/sapi4.py | 46 +++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/source/speech/speech.py b/source/speech/speech.py index 8d1032af0e5..d8d31bcf9d8 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -18,12 +18,8 @@ import controlTypes from controlTypes import OutputReason, TextPosition from controlTypes.state import State -import globalVars -from gui.message import MessageDialog -import gui.contextHelp -import queueHandler import tones -from synthDriverHandler import SynthDriver, getSynth, synthChanged +from synthDriverHandler import getSynth import re import textInfos import speechDictHandler @@ -3064,43 +3060,3 @@ def clearTypedWordBuffer() -> None: complete the word (such as a focus change or choosing to move the caret). """ _curWordChars.clear() - - -def _sapi4DeprecationWarning(synth: SynthDriver, audioOutputDevice: str, isFallback: bool): - """A synthChanged event handler to alert the user about the deprecation of SAPI4.""" - - def setShown(): - synth._hasWarningBeenShown = True - synth.saveSettings() - - def impl(): - MessageDialog( - parent=None, - message=_( - # Translators: Message warning users that SAPI4 is deprecated. - "Microsoft Speech API version 4 is obsolete. " - "Using this speech synthesizer may pose a security risk. " - "This synthesizer driver will be removed in NVDA 2026.1. " - "You are strongly encouraged to choose a more modern speech synthesizer. " - "Consult the Supported Speech Synthesizers section in the user guide for suggestions.", - ), - # Translators: Title of a message dialog. - title=_("Warning"), - buttons=None, - ).addOkButton( - callback=setShown, - ).addHelpButton( - # Translators: A button in a dialog. - label=_("Open user guide"), - callback=lambda: gui.contextHelp.showHelp("SupportedSpeechSynths"), - ).Show() - - if (not isFallback) and (synth.name == "sapi4") and (not getattr(synth, "_hasWarningBeenShown", False)): - # We need to queue the dialog to appear, as wx may not have been initialised the first time this is called. - queueHandler.queueFunction(queueHandler.eventQueue, impl) - - -if not globalVars.appArgs.secure: - # Don't warn users about SAPI4 deprecation in secure mode. - # This stops the dialog appearing on secure screens and when secure mode has been forced. - synthChanged.register(_sapi4DeprecationWarning) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 2521685666c..2858e41e789 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -12,7 +12,11 @@ from ctypes.wintypes import DWORD, HANDLE, WORD from typing import Optional from autoSettingsUtils.driverSetting import BooleanDriverSetting -from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking +import globalVars +import gui.contextHelp +import gui.message +import queueHandler +from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking, synthChanged from logHandler import log import warnings from ._sapi4 import ( @@ -477,3 +481,43 @@ def _mmDeviceEndpointIdToWaveOutId(targetEndpointId: str) -> int: # No matching device found, or default requested explicitly. # Return the ID of Microsoft Sound Mapper return -1 + + +def _sapi4DeprecationWarning(synth: SynthDriver, audioOutputDevice: str, isFallback: bool): + """A synthChanged event handler to alert the user about the deprecation of SAPI4.""" + + def setShown(): + synth._hasWarningBeenShown = True + synth.saveSettings() + + def impl(): + gui.message.MessageDialog( + parent=None, + message=_( + # Translators: Message warning users that SAPI4 is deprecated. + "Microsoft Speech API version 4 is obsolete. " + "Using this speech synthesizer may pose a security risk. " + "This synthesizer driver will be removed in NVDA 2026.1. " + "You are strongly encouraged to choose a more modern speech synthesizer. " + "Consult the Supported Speech Synthesizers section in the user guide for suggestions.", + ), + # Translators: Title of a message dialog. + title=_("Warning"), + buttons=None, + ).addOkButton( + callback=setShown, + ).addHelpButton( + # Translators: A button in a dialog. + label=_("Open user guide"), + callback=lambda: gui.contextHelp.showHelp("SupportedSpeechSynths"), + ).Show() + + if (not isFallback) and (synth.name == "sapi4") and (not getattr(synth, "_hasWarningBeenShown", False)): + # We need to queue the dialog to appear, as wx may not have been initialised the first time this is called. + queueHandler.queueFunction(queueHandler.eventQueue, impl) + + +if not globalVars.appArgs.secure: + # Don't warn users about SAPI4 deprecation in secure mode. + # This stops the dialog appearing on secure screens and when secure mode has been forced. + synthChanged.register(_sapi4DeprecationWarning) From 834ee951979671e7d0d20a7a1ed322c7fbd4591a Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:37:21 +1100 Subject: [PATCH 23/25] Restored files that should no longer be changed --- source/config/configSpec.py | 2 +- source/speech/speech.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/source/config/configSpec.py b/source/config/configSpec.py index d7fd524ce8e..8a6a082dab1 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2025 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt, +# Copyright (C) 2006-2024 NV Access Limited, Babbage B.V., Davy Kager, Bill Dengler, Julien Cochuyt, # Joseph Lee, Dawid Pieper, mltony, Bram Duvigneau, Cyrille Bougot, Rob Meredith, # Burman's Computer and Education Ltd., Leonard de Ruijter, Łukasz Golonka # This file is covered by the GNU General Public License. diff --git a/source/speech/speech.py b/source/speech/speech.py index d8d31bcf9d8..49992c0b03f 100644 --- a/source/speech/speech.py +++ b/source/speech/speech.py @@ -1,7 +1,7 @@ # A part of NonVisual Desktop Access (NVDA) # This file is covered by the GNU General Public License. # See the file COPYING for more details. -# Copyright (C) 2006-2025 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, +# Copyright (C) 2006-2024 NV Access Limited, Peter Vágner, Aleksey Sadovoy, Babbage B.V., Bill Dengler, # Julien Cochuyt, Derek Riemer, Cyrille Bougot, Leonard de Ruijter, Łukasz Golonka """High-level functions to speak information.""" @@ -11,7 +11,6 @@ import weakref import unicodedata import time - import colors import api from annotation import _AnnotationRolesT From 0e09b4b82a8068988b117119a9fa670731ad2e73 Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:55:31 +1100 Subject: [PATCH 24/25] Update source/synthDrivers/sapi4.py Co-authored-by: Sean Budd --- source/synthDrivers/sapi4.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 2858e41e789..b5ac4d0cbca 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -499,7 +499,7 @@ def impl(): "Using this speech synthesizer may pose a security risk. " "This synthesizer driver will be removed in NVDA 2026.1. " "You are strongly encouraged to choose a more modern speech synthesizer. " - "Consult the Supported Speech Synthesizers section in the user guide for suggestions.", + "Consult the Supported Speech Synthesizers section in the User Guide for suggestions. ", ), # Translators: Title of a message dialog. title=_("Warning"), From 1916f0fd39ce416f1949e4576d69b141427b06ff Mon Sep 17 00:00:00 2001 From: Sascha Cowley <16543535+SaschaCowley@users.noreply.github.com> Date: Tue, 21 Jan 2025 17:21:54 +1100 Subject: [PATCH 25/25] Only exclude the warning when running on a secure desktop --- source/synthDrivers/sapi4.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index b5ac4d0cbca..f1a72adf189 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -12,13 +12,13 @@ from ctypes.wintypes import DWORD, HANDLE, WORD from typing import Optional from autoSettingsUtils.driverSetting import BooleanDriverSetting -import globalVars import gui.contextHelp import gui.message import queueHandler from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking, synthChanged from logHandler import log import warnings +from utils.security import isRunningOnSecureDesktop from ._sapi4 import ( MMSYSERR_NOERROR, CLSID_MMAudioDest, @@ -517,7 +517,6 @@ def impl(): queueHandler.queueFunction(queueHandler.eventQueue, impl) -if not globalVars.appArgs.secure: - # Don't warn users about SAPI4 deprecation in secure mode. - # This stops the dialog appearing on secure screens and when secure mode has been forced. +if not isRunningOnSecureDesktop(): + # Don't warn users about SAPI4 deprecation when running on a secure desktop. synthChanged.register(_sapi4DeprecationWarning)