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)