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 8 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/autoSettingsUtils/driverSetting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 2 additions & 44 deletions source/speech/speech.py
Original file line number Diff line number Diff line change
@@ -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."""
Expand All @@ -11,18 +11,14 @@
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
import globalVars
from gui.message import MessageDialog
import queueHandler
import tones
from synthDriverHandler import SynthDriver, getSynth, synthChanged
from synthDriverHandler import getSynth
import re
import textInfos
import speechDictHandler
Expand Down Expand Up @@ -3063,41 +3059,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():
config.conf["speech"]["_hasSapi4WarningBeenShown"] = True

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.",
),
# Translators: Title of a message dialog.
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)


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)
2 changes: 1 addition & 1 deletion source/synthDrivers/_sapi4.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 51 additions & 3 deletions source/synthDrivers/sapi4.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
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 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 (
Expand Down Expand Up @@ -128,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,
Expand Down Expand Up @@ -458,7 +466,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,
)
Expand All @@ -473,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.",
SaschaCowley marked this conversation as resolved.
Show resolved Hide resolved
),
# 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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important this is announced in other forms of secure mode too, as some users are daily drivers of this, and it's important secure context users get warned about this. they can get their admin to disable it by disabling secure mode temporarily.

I just don't think it should be done on secure screens (e.g password, UAC), as it is a forever nag because a user wouldn't be able to save the settings on secure screens directly. With this new behaviour of saving the variable, is it still saved the same way? does the nag always happen on secure mode?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag is still saved to config, it's just now done via the SAPI4 SynthDriver rather than as part of the config schema directly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've now updated it to show the warning in all cases except when running on a secure desktop.

# 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)
1 change: 1 addition & 0 deletions user_docs/en/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down