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/source/synthDrivers/_sapi4.py b/source/synthDrivers/_sapi4.py index 5d6c1586643..5480b8d144f 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. @@ -20,6 +18,7 @@ Structure, ) from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD +from enum import IntEnum from comtypes import GUID, IUnknown, STDMETHOD import winKernel @@ -227,3 +226,19 @@ class ITTSNotifySinkW(IUnknown): CLSID_MMAudioDest = GUID("{CB96B400-C743-11cd-80E5-00AA003E4B50}") 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 + """ + + QUERY_INSTANCE_ID = 2065 + """DRV_QUERYFUNCTIONINSTANCEID """ + + QUERY_INSTANCE_ID_SIZE = 2066 + """DRV_QUERYFUNCTIONINSTANCEIDSIZE """ diff --git a/source/synthDrivers/sapi4.py b/source/synthDrivers/sapi4.py index 3617530ca63..f1a72adf189 100755 --- a/source/synthDrivers/sapi4.py +++ b/source/synthDrivers/sapi4.py @@ -1,18 +1,26 @@ # 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 more details. +# This module is deprecated, pending removal in NVDA 2026.1. import locale from collections import OrderedDict, deque 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, windll +from ctypes.wintypes import DWORD, HANDLE, WORD from typing import Optional -from synthDriverHandler import SynthDriver, VoiceInfo, synthIndexReached, synthDoneSpeaking +from autoSettingsUtils.driverSetting import BooleanDriverSetting +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, CLSID_TTSEnumerator, IAudioMultiMediaDevice, @@ -34,9 +42,9 @@ TTSFEATURE_VOLUME, TTSMODEINFO, VOICECHARSET, + DriverMessage, ) import config -import nvwave import weakref from speech.commands import ( @@ -52,6 +60,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] @@ -122,7 +133,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, @@ -286,7 +300,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["audio"]["outputDevice"])) if self._ttsCentral: self._ttsCentral.UnRegister(self._sinkRegKey) self._ttsCentral = POINTER(ITTSCentralW)() @@ -421,3 +435,88 @@ 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: + """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() + # 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( + HANDLE(devID), + DriverMessage.QUERY_INSTANCE_ID_SIZE, + 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), + DriverMessage.QUERY_INSTANCE_ID, + 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 + # 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 isRunningOnSecureDesktop(): + # Don't warn users about SAPI4 deprecation when running on a secure desktop. + synthChanged.register(_sapi4DeprecationWarning) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 52fefa2bc17..0828ab897b7 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -4,6 +4,9 @@ ### Important notes +* 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 * Support for math in PDFs has been added. @@ -179,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 @@ -190,6 +194,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, `synthDrivers.sapi4`, is deprecated and planned for removal in 2026.1. (#17599) ## 2024.4.2 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.