Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix SAPI 4 driver #17599

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 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
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)
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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):
SaschaCowley marked this conversation as resolved.
Show resolved Hide resolved
"""A synthChanged event handler to alert the user about the deprecation of SAPI4."""

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

def impl():
MessageDialog(
seanbudd marked this conversation as resolved.
Show resolved Hide 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",
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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
SaschaCowley marked this conversation as resolved.
Show resolved Hide resolved
# Defined in mmsyscom.h
MMSYSERR_NOERROR = 0

# Function prototypes
# Defined in mmeapi.h
winmm = windll.winmm
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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)
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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.
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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:
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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.
SaschaCowley marked this conversation as resolved.
Show resolved Hide 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.
SaschaCowley marked this conversation as resolved.
Show resolved Hide resolved

## 2024.4.2

Expand Down