Skip to content

Support for 32 bit sapi4 and sapi5 via a 32 bit synthDriver runtime#19432

Merged
SaschaCowley merged 74 commits into
nvaccess:betafrom
michaelDCurran:sapi32
Jan 27, 2026
Merged

Support for 32 bit sapi4 and sapi5 via a 32 bit synthDriver runtime#19432
SaschaCowley merged 74 commits into
nvaccess:betafrom
michaelDCurran:sapi32

Conversation

@michaelDCurran

Copy link
Copy Markdown
Member

Link to issue number:

Replaces pr #19412

Summary of the issue:

With the switch to 64 bit, NVDA no longer supports sapi4, or 32 bit specific sapi5 synthesizers. NVDA should somehow continue to support these synthesizers.

Description of user facing changes:

NvDA continues to support sapi4 and 32 bit specific Sapi5 synthesizers.

Description of developer facing changes:

Description of development approach:

  • Compile nvdaHelperLocal for all archetectures, rather than just for NVDA's core archetecture (x64). NVDA's 32 bit synthDriver host runtime needs nvdaHelperLocal for WASAPI.

  • Move the wasapi function definitions for nvdaHelperLocal.dll into their own 'wasapi' Python module, so that they can be imported separate to NvDAHelper. NVDA's 32 bit synthDriver host runtime uses WASAPI directly.

  • Compile sonic independently of eSpeak and for all archetectures (not just x64), as it is needed by sapi5 when running in NVDA's 32 bit synthDriver host runtime. The x86 sonic dll will be placed in source/synthDrivers32.

  • winBindings.kernel32: add definition for GetCurrentProcessId. Used in logging for NVDA's 32 bit synthDriver host runtime.

  • Add a jobObject module, which contains a Job class which wraps a win32 Job object, which allows automatically killing any process associated with it. This is sued to ensure that any process started for the 32 bit synthDriver host runtime is killed off if NvDA exits.

  • Add a _bridge package, which provides a client and runtime for the 32 bit synthDriver host. The client is code that runs in NVDA, and the runtime is code that runs in its own 32 bit child process. Communication between NvDA and the child process is via RPYC over the child processs's standard pipe handles. This _bridge package can be later extended with more clients and runtimes as we start to implement a full ART.

  • Add 32 bit sapi4 and sapi5 synthDrivers which use the synthDriverHost32 runtime.

Testing strategy:

Manually tested running NvDA with both 32 bit sapi4 (Truevoice and MS Mike Mary Sam) and 32 bit sapi5 (Windows built-in David, eSpeak bridge, Mikropuhe). Ran with NvDA launcher, installed normal user, and on secure desktop (UAC and logon screen).

Known issues with pull request:

  • This PR only supports sapi4 and 32 bit sapi5 quite specifically. It is not a generalized 32 bit shim for add-ons. To do this, we would need to include the security features from pr Support for 32 bit sapi4 and sapi5 via a 32 bit synthDriver runtime  #19412 and greatly increase the size of the runtime, and add many more proxies and services. In other words, complete the secure ART project ;). To have something ready for 2026.1, we have decided to limit this to sapi4 and 32 bit sapi5.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

Copilot AI review requested due to automatic review settings January 11, 2026 09:10
@michaelDCurran michaelDCurran requested a review from a team as a code owner January 11, 2026 09:10
Comment thread source/_synthDrivers32/_sapi4.py
Comment thread source/_synthDrivers32/sapi4.py
Comment thread source/wasapi.py
Comment thread source/_bridge/runtimes/synthDriverHost/config.py

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for 32-bit SAPI4 and SAPI5 synthesizers in 64-bit NVDA by implementing a 32-bit synth driver host runtime that runs in a separate 32-bit process. Communication between NVDA and the 32-bit process occurs via RPYC over standard pipes.

Changes:

  • Implements a bridge architecture with services, proxies, and connections to support remote synth drivers
  • Extracts WASAPI functions into a standalone module for use by both NVDA and the 32-bit runtime
  • Adds job object support to ensure child processes are terminated when NVDA exits

Reviewed changes

Copilot reviewed 41 out of 47 changed files in this pull request and generated 22 comments.

Show a summary per file
File Description
source/winBindings/kernel32.py Added Win32 API bindings for job objects and GetCurrentProcessId
source/winBindings/jobapi2.py New module defining job object structures and constants
source/wasapi.py Extracted WASAPI function definitions from NVDAHelper for standalone use
source/jobObject.py New module providing Job class wrapper for Windows job objects
source/_bridge/* New bridge infrastructure for client-server architecture
source/synthDrivers32/* 32-bit SAPI4 and SAPI5 synth driver implementations
source/synthDrivers/sapi4_32.py Proxy driver for 32-bit SAPI4
source/synthDrivers/sapi5_32.py Proxy driver for 32-bit SAPI5
nvdaHelper/archBuild_sconscript Build changes to compile nvdaHelperLocal and sonic for all architectures
runtime-builders/synthDriverHost32/* Build setup for 32-bit runtime executable

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/testAndPublish.yml
Comment thread source/_bridge/base.py
Comment thread source/winBindings/kernel32.py
Comment thread source/_bridge/runtimes/synthDriverHost/globalVars.py Outdated
Comment thread source/synthDrivers32/sapi5.py Outdated
Comment thread source/synthDrivers32/sapi4.py
Comment thread source/synthDrivers32/sapi4.py
Comment thread source/synthDrivers32/sapi4.py
Comment thread source/_bridge/runtimes/synthDriverHost/main.pyw
Comment thread source/synthDrivers32/sapi4.py
@SaschaCowley SaschaCowley merged commit f32e049 into nvaccess:beta Jan 27, 2026
75 checks passed
@LeonarddeR

LeonarddeR commented Jan 27, 2026

Copy link
Copy Markdown
Collaborator

I'm seeing an issue with this, at least I assume they have to do with this pr, namely that profile switching is broken due to an upgrade failure:

ERROR - config.ProfileTrigger.enter (16:08:50.860) - MainThread (12728):
Error entering trigger app:notepad, profile Text Editing
Traceback (most recent call last):
  File "config\__init__.pyc", line 1381, in enter
  File "config\__init__.pyc", line 865, in _triggerProfileEnter
  File "config\__init__.pyc", line 643, in _getProfile
  File "config\__init__.pyc", line 586, in _loadConfig
  File "config\__init__.pyc", line 582, in _loadConfig
  File "config\profileUpgrader.pyc", line 26, in upgrade
  File "config\profileUpgrader.pyc", line 42, in _doConfigUpgrade
  File "config\profileUpgradeSteps.pyc", line 642, in upgradeConfigFrom_20_to_21
  File "configobj\__init__.pyc", line 496, in __getitem__
KeyError: 'synth'

SaschaCowley pushed a commit that referenced this pull request Jan 28, 2026
…key or value does not exist E.g. for a config profile. (#19519)

Addresses an issue introduced by pr #19432

### Summary of the issue:
With the merging of pr #19432 which adds SAPI 32 bit support, An error
is produced when switching to config profiles that do not have
particular synth keys already set.
```
ERROR - config.ProfileTrigger.enter (16:08:50.860) - MainThread (12728):
Error entering trigger app:notepad, profile Text Editing
Traceback (most recent call last):
  File "config\__init__.pyc", line 1381, in enter
  File "config\__init__.pyc", line 865, in _triggerProfileEnter
  File "config\__init__.pyc", line 643, in _getProfile
  File "config\__init__.pyc", line 586, in _loadConfig
  File "config\__init__.pyc", line 582, in _loadConfig
  File "config\profileUpgrader.pyc", line 26, in upgrade
  File "config\profileUpgrader.pyc", line 42, in _doConfigUpgrade
  File "config\profileUpgradeSteps.pyc", line 642, in upgradeConfigFrom_20_to_21
  File "configobj\__init__.pyc", line 496, in __getitem__
KeyError: 'synth'
```
 
### Description of user facing changes:
No error is produced and profiles load correctly.

### Description of developer facing changes:

### Description of development approach:
* `config.profileUpgradeSteps.upgradeConfigFrom_20_to_21`: Fetch
existing keys with `get` rather than key lookup.
 
### Testing strategy:
* Starting with a main config schema of 20 that contained `synth =
sapi4`, and both sapi4 and sapi5 sections in speech, ensured that the
synth value was changed to sapi4_32, and that the sapi4 section was
renamed to sapi4_32, and that the sapi5 section was renamed to sapi5_32
when running NvDA, and that NVDA successfully used the sapi4_32 synth.
* Started with a config profile for notepad at schema 20, which had no
speech keys or values. Creted just with a change to braille config.
Running NvDA, switching to notepad, ensured that the error reported
above is gone, and that the profile loaded okay.

### Known issues with pull request:
None known.
@seanbudd

Copy link
Copy Markdown
Member

@SaschaCowley or @michaelDCurran - can you please announce these API changes to the mailing list for this and #19436?

@seanbudd seanbudd mentioned this pull request Jan 28, 2026
5 tasks
SaschaCowley pushed a commit that referenced this pull request Feb 3, 2026
…bit SAPI synths (#19541)

Fixes #19529

### Summary of the issue:
PR #19432 added back support for 32 bit sapi synths via a 32 bit shim.
However, in-stream synth parameter changes (E.g. pitch changes for
capital letters) was not implemented, and therefore pitch would never
change when speaking a capital letter.

### Description of user facing changes:

### Description of developer facing changes:

### Description of development approach:
* synthDriverHost: when creating a synth driver and returning it to
NVDA, also locally track it as the active synth in the host by setting
synthDriverHandler._curSynth. this allows synthDriverhandler.getsynth()
to return the synth inside the 32 bit host. Needed to fetch the synth's
name for config value lookup via synth commands.**
* Support all built-in synth commands (I.e. index, break, langChange,
characterMode, pitch, rate, volume and phoneme) on 32 bit synthDrivers
proxied to NvDA. This includes correctly expsing the supportedCommands
property, and a much better serialization / deserialization in speak,
which no longer uses Pickle, but instead json, fully supporting all the
synth commands. This lallows pitch changes for capital letters among
other things.**
* sapi4 synthDriver: when fetching rate, pitch and volume values, ensure
that the raw values are clipped to the min and max values before
converting to percentage. Sometimes the raw value was outside the min
max range and was producing values higher than 100%.
* Add missing rateBoost getter / setter on SynthDriver proxy. This
allows rateBoost to fully function on 32 bit sapi5.

### Testing strategy:
* Switch to sapi4.
* Open notepad.
* Type `Hh`. (The first h being capitalized).
* Use left and right arrows to move to each character, having NVDA speak
it. Ensure that the pitch is raised for the capital h and back to normal
for the second h.

### Known issues with pull request:
None known
SaschaCowley pushed a commit that referenced this pull request Feb 4, 2026
Replaces #19515
Follow up to #19432 

### Summary of the issue:
#19432 used an older version of setuptools which contains security
vulnerabilities

### Description of user facing changes:
none

### Description of developer facing changes:
bump setuptools
### Description of development approach:
bump setuptools

### Testing strategy:
none

### Known issues with pull request:
none

---------

Co-authored-by: WMHN <1872265132@qq.com>
SaschaCowley pushed a commit that referenced this pull request Feb 5, 2026
#19562)

Closes #19557

### Summary of the issue:
When using 32 bit sapi synthDrivers introduced with pr #19432,
particular strings, such as the display name of driver settings, when
moving through the synth settings ring, are not translated into the
current NVDA language.
 
### Description of user facing changes:
The translations are now used, if available.

### Description of developer facing changes:

### Description of development approach:
* Expose a getAppArg method on the NvDA service, which the
synthDriverHost runtime can use to fetch a particular NvDA commandline
argument value.
* synthDriverHost runtime: Sync the language key from the general
section of NVDA's configuration to the synthDriverHost's local copy of
the configuration.
* synthDriverHost: appropriately set its local copy of
globalVars.appArgs.language to NVDA's value, using the NVDA Service's
getAppArg method.
* SynthDriverHost: set the language based on the language in the config
/ the overridden commandline argument value, using the same logic that
NvDA uses in core.
 
### Testing strategy:
* Set NvDA's language to German. Select the sapi4 synthesizer. Move left
and right through the synth settings ring, ensuring that parameters such
as rate and pitched are correctly announced with their German translated
names.
* Do the above steps but instead of setting the configured language,
override on the commandline with `--lang de`.

### Known issues with pull request:
None known.
seanbudd pushed a commit that referenced this pull request Feb 13, 2026
…19609)

Fixes #19594
Summary of the issue:

With the introduction of 32 bit sapi support in pr #19432, an occasional freeze may be seen when using 32 bit sapi synths. Specifically when cancelling speech at the same time a speech utterance was finishing.
NVDA would freeze in its call to synth.cancel and the synthDriver process would freeze when calling the synthIndexReached or synthDoneSpeaking callbacks.
This seems to be related to RPYC issue #345, where if a remote call is being made on one thread, but another thread is also moving a remote call and or somehow serving or polling the connection, the second thread may accidentally read and eat the return of the call in the first thread, thus causing the call on the first thread to time out.
Description of user facing changes:
Description of developer facing changes:
Description of development approach:

    the synthDriver process now makes all synthIndexReached and synthDoneSpeaking notification calls back to NvDA asynchronicely, rather than blocking. This ensures that the synth driver process is now no longer waiting on the connection for these returns, greatly reducing the chance of a freeze, as now all blocking calls should only be coming in from NVDA, not going out.
SaschaCowley pushed a commit that referenced this pull request Feb 23, 2026
…playing directly. (#19577)

### Summary of the issue:
the 32 bit sapi synthDrivers added in pr #19432 currently play audio
directly. Although this provides good performance and works in most
scenarios, it does not allow supporting audio ducking (as the host
process does not have permission to duck audio, and if NvDA did
ititself, it would inappropriately duck the wrong thing). Also, as we
start moving to a secure add-on runtime, 3rd party synthDrivers (at
least on secure desktops) will most likely be run in an appContainer,
which unfortunately deos not allow the host process to access tha audio
device directly. thus, to work around both of these issues, the 32 bit
sapi synthDrivers should send the audio to NvDA for playback.

### Description of development approach:
Take more code from pr #19412, specifically the WavePlayer service and
proxy, the pipe creation code, and raiiUtils.
NvDA exposes a WavePlayer service, allowing an external process such as
the 32 bit sapi synthDriver processes to send audio.
 
### Testing strategy:
General smoke testing of sapi4 and 32 bit sapi5 synthDrivers.

### Known issues with pull request:
* the Mikropuhe sapi5 voices sound a bit scratchy / jittery like there
are buffer underruns. I may require some assistance in improving the
stability / performance here. Sapi4 truevoice seems to work fine, sapi5
espeak and in-built windows sapi5 voices also seem to be fine.
SaschaCowley pushed a commit that referenced this pull request Feb 23, 2026
…#19665)

Partial fix for #19618

### Summary of the issue:
As 32 bit sapi synthDrivers introduced in pr #19432 produce audio
directly in their own process, NVDA currently cannot correctly duck
audio when they are in use. Specifically, if NVDA is set to always duck,
the audio from these synths is ducked along with other external audio.
And if set to duck for speech and sounds, their audio does not cause
ducking, and any NvDA sound that does, ducks their audio.
The correct approach to fix this for the long-term is to broker all 32
bit audio through NVDA, rather than it being played directly by the
external process. See pr #19577. But until then, we should at least
consider tempoarily disabling audio ducking while one of these
synthDrivers is in use, so that its audio is not inappropriately ducked. 

### Description of development approach:
* Added a new private `_AudioDuckingSuspender` class to `audioDucking`
which when at least one instance exists, temporarily suspends audio
ducking, and disallows changing the current audio ducking setting via
the gesture or GUI setting. When all instances are deleted, then audio
ducking is restored back to the state it was before one or more
instances were created.
* `_bridge`'s `SynthDriverProxy` class when instantiated now creates an
instance of `_AudioDuckingsuspender` and holds it on the
SynthDriverProxy instance, thus causing audio ducking to be temporarily
disabled while this synthDriver is in use.
 
### Testing strategy:
With a copy of NVDA that supports audio ducking:
* Using eSpeak, set audio ducking via the gesture to always duck.
Confirm that audio stays ducked.
* Choose the sapi 32 bit synth from the Select Synthesizer dialog.
Confirm that audio is no longer ducked.
* Try to cycle through audio ducking modes with `NVDA+shift+d`. Confirm
that NvDA reports that audio ducking is not supported.
* Go to the audio pannel in the NvDA settings dialog. Confirm that the
Audio ducking mode control is disabled.
* Choose eSpeak from the Selected synthesizer dialog. Confirm that audio
is again ducked.
* Try to cycle through the audio ducking modes with the gesture. Confirm
that this works.
* Confirm that the audio ducking mode in the Audio panel of the NvDA
settings dialog is no longer disabled.

### Known issues with pull request:
* This is a temporary partial fix that just ensures that audio ducking
is correctly disabled. The full fix is to broker audio and again fully
support audio ducking. PR #19432.
* An alternative would be to grant the 32 bit synthDriver runtime
process UIAccess and build audio ducking directly into it. However,
adding a second process with UIAccess would greatly increase our
possible attack surface, and is not moving in the direction of a secure
add-on runtime.
seanbudd pushed a commit that referenced this pull request Feb 25, 2026
…ble and don't include unnecessary pdb lib and exp files (#19683)

Link to issue number:

Fixes #19654
Fixes #19653
Summary of the issue:

The 32 bit synthDriverhost runtime introduced in PR #19432:

    Does not contain correct version information in its executable,
    Is not signed, and
    includes some extra lib exp and pdb files which are not necessary and just take up space.

Description of development approach:

    synthDriverHost's setup-runtime.py py2exe script now takes version and publisher as commandline arguments, so that these can be set on the executable dynamically.
    The github workflow and sconstruct pass the correct version and publisher to the setup-runtime.py py2exe script.
    When building the main dist target with scons, sign nvda_synthDriverHost.exe if it exists.
    When building the main NvDA distribution with py2exe, only copy py and dll files in the _synthDrivers32 directory, which now ignores lib exp and pdb files.
SaschaCowley pushed a commit that referenced this pull request Mar 9, 2026
…es, excluding lib exp and pdb files which are not needed. (#19748)

Fixes #19653

### Summary of the issue:
The 32 bit synthDriverhost runtime introduced in PR #19432 includes some
extra lib exp and pdb files which are not necessary and just take up
space.

### Description of user facing changes:
Slightly reduces the size of NVDA.

### Description of developer facing changes:

### Description of development approach:
When building the main NvDA distribution with py2exe, only copy py and
dll files in the _synthDrivers32 directory, which now ignores lib exp
and pdb files.

### Testing strategy:
* [x] Build locally, ensuring that the unneeded lib exp and pdb files
are not included in the _synthDrivers32 directory. Ensure NvDA runs and
that the sapi4 and 32 bit sapi5 synthDrivers can be selected and used.
* [x] Create a try build, also ensuring all of the above.

### Known issues with pull request:
None known.

### Code Review Checklist:
- [x] Documentation:
  - Change log entry
  - User Documentation
  - Developer / Technical Documentation
  - Context sensitive help for GUI changes
- [x] Testing:
  - Unit tests
  - System (end to end) tests
  - Manual testing
- [x] UX of all users considered:
  - Speech
  - Braille
  - Low Vision
  - Different web browsers
  - Localization in other languages / culture than English
- [x] API is compatible with existing add-ons.
- [x] Security precautions taken.
SaschaCowley pushed a commit that referenced this pull request Mar 9, 2026
…xists. (#19749)

### Summary of the issue:
The 32 bit synthDriverhost runtime executable introduced in PR #19432 is
not signed.
 
### Description of user facing changes:

### Description of developer facing changes:

### Description of development approach:
When building the main dist target with scons, also sign
nvda_synthDriverHost.exe if it exists.
This is an improvement over the previous try in pr #19683, where now the
path is passed into the lambda by value so that it does not change in
the for loop after being captured.

### Testing strategy:
* [x] Compiled with scons synthDriverHost32Runtime and scons dist,
providing a signing certificate
  *Confirmed that nvda_synthDriverHost.exe was signed.
* [x] Run the above tests on a try build from this pr.

### Known issues with pull request:
None known.
SaschaCowley pushed a commit that referenced this pull request Mar 10, 2026
…#19762)

Fixes #19654

### Summary of the issue:
The 32 bit synthDriverhost runtime introduced in PR #19432 Does not
contain correct version information in its executable.

### Description of user facing changes:

### Description of developer facing changes:

### Description of development approach:
* synthDriverHost's setup-runtime.py py2exe script now takes version and
publisher as commandline arguments, so that these can be set on the
executable dynamically.
* The github workflow and sconstruct pass the correct version and
publisher to the setup-runtime.py py2exe script.

### Testing strategy:
* [x] Locally compiled with scons synthDriverHost32Runtime, providing
custom version and publisher. Confirmed that the synthDriverHost
executable contains correct version and publisher information.
* [x] Create try bild. Confirming the same as above.

### Known issues with pull request:
None known.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conceptApproved Similar 'triaged' for issues, PR accepted in theory, implementation needs review. release/blocking this issue blocks the milestone release release/blocking-beta

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants