Support for 32 bit sapi4 and sapi5 via a 32 bit synthDriver runtime#19432
Merged
Conversation
…api' module, so that they can be imported separte to NvDAHelper.
…or at least also x86 with the 32 bit shim to access wasapi functions.
…ill be needed by the 32 bit shim. The x86 sonic dll will be placed in source/synthDrivers32.
…2 Job object, which allows automaticlaly killing any process associated with it. Will be required for the NVDA bridge.
…hDriver 32 bit shim process.
…ns synthDriverHost32Runtime
michaelDCurran
commented
Jan 11, 2026
michaelDCurran
commented
Jan 11, 2026
michaelDCurran
commented
Jan 11, 2026
michaelDCurran
commented
Jan 11, 2026
Contributor
There was a problem hiding this comment.
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.
…iver host runtime, rather than having them committed twice.
…r may not be there depending on how Proxy might be used as a minxin.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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: |
5 tasks
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.
Member
|
@SaschaCowley or @michaelDCurran - can you please announce these API changes to the mailing list for this and #19436? |
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>
5 tasks
5 tasks
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.
Merged
5 tasks
5 tasks
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.
5 tasks
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.
7 tasks
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.
This was referenced Feb 26, 2026
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.
7 tasks
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.
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Code Review Checklist: