Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
df45ca7
Initial implementation of translation between mmDevice endpoint ID st…
SaschaCowley Jan 8, 2025
16b377d
Slight improvements to typing
SaschaCowley Jan 8, 2025
3e2794b
Fix incorrect config path
SaschaCowley Jan 8, 2025
0bee92c
Merge branch 'master' into fixSapi4
SaschaCowley Jan 8, 2025
173d75d
Merge branch 'master' into fixSapi4
SaschaCowley Jan 8, 2025
6bf3f01
Added a warning when SAPI4 is in use.
SaschaCowley Jan 8, 2025
c7d363b
Changelog
SaschaCowley Jan 8, 2025
6da072f
Update copyright headers
SaschaCowley Jan 8, 2025
d0781ba
Update changes
SaschaCowley Jan 8, 2025
2af45cf
Switch to a driver message enum
SaschaCowley Jan 8, 2025
ef06304
Move loading winmm into _mmDeviceEndpointIdToWaveOutId
SaschaCowley Jan 8, 2025
8c7007a
Documentation improvements
SaschaCowley Jan 9, 2025
48b4d6e
Add deprecation warning
SaschaCowley Jan 9, 2025
181b36a
Added note about SAPI4's deprecation to the UG
SaschaCowley Jan 9, 2025
d277513
Only show warning when not minimal
SaschaCowley Jan 9, 2025
306bd86
Mark hasSapi4WarningBeenShown as private
SaschaCowley Jan 9, 2025
b3bf403
Make SAPI4 warning translatable
SaschaCowley Jan 9, 2025
9ae4429
Merge branch 'master' into fixSapi4
SaschaCowley Jan 16, 2025
3b9c2bc
Don't show DriverSettings with ids that start with a _ in the GUI
SaschaCowley Jan 16, 2025
04f9a78
Switch to using a SAPI4 setting for the warning
SaschaCowley Jan 16, 2025
f08a487
Fixed accidental double underscore
SaschaCowley Jan 16, 2025
53a2d71
Change to using secure instead of minimal
SaschaCowley Jan 16, 2025
41774e9
Set _hasWarningBeenShown directly
SaschaCowley Jan 20, 2025
6331ee1
Add an "Open user guide" button and more explanatory text to the SAPI…
SaschaCowley Jan 20, 2025
1e2ae40
Moved the sapi4 deprecation warning code to synthDrivers.sapi4.
SaschaCowley Jan 20, 2025
834ee95
Restored files that should no longer be changed
SaschaCowley Jan 20, 2025
0e09b4b
Update source/synthDrivers/sapi4.py
SaschaCowley Jan 21, 2025
1916f0f
Only exclude the warning when running on a secure desktop
SaschaCowley Jan 21, 2025
f8c7d2a
Merge branch 'master' into fixSapi4
SaschaCowley Jan 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
autoDialectSwitching = boolean(default=false)
delayedCharacterDescriptions = boolean(default=false)
excludedSpeechModes = int_list(default=list())
hasSapi4WarningBeenShown = boolean(default=False)
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved

[[__many__]]
capPitchChange = integer(default=30,min=-100,max=100)
Expand Down
36 changes: 35 additions & 1 deletion source/speech/speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
"""A synthChanged event handler to alert the user about the deprecation of SAPI4."""

def setShown():
config.conf["speech"]["hasSapi4WarningBeenShown"] = True

def impl():
MessageDialog(
Comment thread
seanbudd marked this conversation as resolved.
Resolved
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",
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
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)
23 changes: 20 additions & 3 deletions source/synthDrivers/_sapi4.py
Original file line number Diff line number Diff line change
@@ -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 <http://www.nvda-project.org/>
# 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.

Expand All @@ -18,6 +16,7 @@
POINTER,
sizeof,
Structure,
windll,
)
from ctypes.wintypes import BYTE, DWORD, LPCWSTR, WORD
from comtypes import GUID, IUnknown, STDMETHOD
Expand Down Expand Up @@ -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
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
# Defined in mmsyscom.h
MMSYSERR_NOERROR = 0

# Function prototypes
# Defined in mmeapi.h
winmm = windll.winmm
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
waveOutMessage = winmm.waveOutMessage
waveOutMessage.restype = c_uint

waveOutGetNumDevs = winmm.waveOutGetNumDevs
waveOutGetNumDevs.argtypes = []
waveOutGetNumDevs.restype = c_int
48 changes: 43 additions & 5 deletions source/synthDrivers/sapi4.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# A part of NonVisual Desktop Access (NVDA)
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
# 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.
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved

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,
Expand All @@ -33,9 +34,12 @@
TTSFEATURE_VOLUME,
TTSMODEINFO,
VOICECHARSET,
waveOutGetNumDevs,
waveOutMessage,
DRV_QUERYFUNCTIONINSTANCEID,
DRV_QUERYFUNCTIONINSTANCEIDSIZE,
)
import config
import nvwave
import weakref

from speech.commands import (
Expand Down Expand Up @@ -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["audio"]["outputDevice"]))
self._ttsCentral = POINTER(ITTSCentralW)()
self._ttsEngines.Select(self._currentMode.gModeID, byref(self._ttsCentral), self._ttsAudio)
self._ttsAttrs = self._ttsCentral.QueryInterface(ITTSAttributes)
Expand Down Expand Up @@ -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:
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
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
4 changes: 4 additions & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved
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.
Expand Down Expand Up @@ -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.
Comment thread
SaschaCowley marked this conversation as resolved.
Resolved

## 2024.4.2

Expand Down