Repository: nvaccess/nvda

Issue:
Error with OneCore voice and automatic language switching in NVDA2022.1
### Context
Issue found while trying to reproduce #13727. Opened as a new issue as per https://github.com/nvaccess/nvda/issues/13727#issuecomment-1135409560.
Cc @seanbudd 

### Steps to reproduce:
* Use OneCore non-English voice (mine is Hortense - French)
* Ensure NVDA has automatic language switching enabled (during my tests, automatic dialect switching is disabled however)
* Open the following URL: `data:text/html,<p>Hello</p><p lang="en">world</p>`
* Read the page

### Actual behavior:
* With NVDA 2021.3.4 (portable), the whole page is read in French since I have no English OneCore voice.
* With NVDA 2022.1 (installed), the word "world" is not read and an error is logged.

Here are the logs for both versions of NVDA:
* [OneCoreWithNVDA2021.3.4.log](https://github.com/nvaccess/nvda/files/8755212/OneCoreWithNVDA2021.3.4.log): OK
* [OneCoreWithNVDA2022.1.log](https://github.com/nvaccess/nvda/files/8755214/OneCoreWithNVDA2022.1.log): Error


### Expected behavior:

OneCore reads the whole page; if no English voice is available the default (French) voice should be used instead.

### !additional note
If reading 5 times the line "world" causing the error, the synth switches to eSpeak. Cf log:
[OneCoreSwitchToESpeak.log](https://github.com/nvaccess/nvda/files/8763530/OneCoreSwitchToESpeak.log)

### System configuration
#### NVDA installed/portable/running from source:
Installed
#### NVDA version:
2022.1 
#### Windows version:
Windows 10 2004 (x64) build 19041.388
#### Name and version of other software in use when reproducing the issue:
N/A
#### Other information about your system:
* Windows French, NVDA language default (thus French)
* In the list of OneCore voices, I have French voices (Hortense, Julie, Paul) and German voices (Stefan, Katja, Hedda) but no English voice. I do not remember if I had never had one, if I have uninstalled it (what is very unlikely but why not for tests), or if this voice has disappeared without my willing (e.g. during an update or an installation of something).

### Other questions
#### Does the issue still occur after restarting your computer?
Yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
As written earlier, 2021.3.4 (portable) does not exhibit this issue.
#### If NVDA add-ons are disabled, is your problem still occurring?
Test done without add-ons
#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes



Discussion:
Hi @CyrilleB79 - I've tried reproducing this with a fresh French Windows 10 installation and had no success. I was using a slightly newer build as your build is not available. Would you be able to updating Windows? Can you try installing and removing English voice packs? Do you know if anyone else has experienced this issue?
Is this reproducible with 2022.1beta1?
I think this aspect of the logs shows that OneCore is partially installed:

```py
DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:50.999) - MainThread (27140):
Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_DavidM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:51.000) - MainThread (27140):
Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_ZiraM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:51.000) - MainThread (27140):
Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
```
I will try to reproduce the issue with NVDA 2022.1beta1

My Windows build is unavailable since I should actually update it. For some reason I have an error and cannot update; I have to take some time to investigate why.

I have had no report of someone else experiencing this issue. I have probably had issue while installing or uninstalling things since the log indicates traces of David, Mark and Zira English US voices.

If you really want I can try to install/uninstall these voices again. But after that, I may be unable to reproduce the issue again.

Actually, my concern is not that I have English voices missing (since I have not yet tested to reinstall them).

My concern regards the following points:
1. There is a regression: my configuration causes an error and no speech with 2022.1 whereas in 2021.3.x the whole text is read without any error.
2. These tests seems to indicate that with NVDA 2021.3.x, NVDA was able to fallback to current voice when auto-switch tried to switch to an invalid voice; with NVDA 2022.1 this causes an error instead.

Additional note: I have not tested, but maybe removing the registry key of a voice on a good install can help test this code path.

Hi @CyrilleB79 - thanks for these notes. I was planning to try and find a way to reproduce this by doing something similar to what you suggested.

I agree that this is still a concerning regression, but if it is not widely spread it has a lower impact than initially considered. 
@seanbudd, I have just tested with NVDA 2022.1beta1 (run from launcher). The issue was already present in this version.

Also, I agree with you: since I am the only one known so far to be impacted by this issue and since I have not yet tested to reinstall and uninstall these voices, the issue is not critical at all.
Moreover, there is a work-around: disable automatic language switching.




@CyrilleB79 - re-opening this as we would still like to fix this
I haven't been able to manage to reproduce this. On a copy of English Windows 10, I tried:

- removing English voices from `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\`
- making the VoicePath of English voices in `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\` point to an invalid location
- removing English voices files from `%windir%\Speech_OneCore\Engines\TTS`
- removing English voices from `HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4`
My guess is this was introduced in #11768 but without being able to reproduce this, I can't bisect alpha and determine the commit which broke this. 
Note that OneCore stores application specific voice keys in Computer\HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated. The ID OneCore generates seems to depend on the executable, for NVDA it's Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4. You may be able to test this by performing your steps in Computer\HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4 instead of the main Speech_OneCore registry key.
> Note that OneCore stores application specific voice keys in Computer\HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated. The ID OneCore generates seems to depend on the executable, for NVDA it's Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4. You may be able to test this by performing your steps in Computer\HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4 instead of the main Speech_OneCore registry key.

That was also tested and I was unable to reproduce the issue. Have you run into this issue @Mohamed00?
Thanks for reopening the issue; actually, closing it was not intentional.
I will bissect the alpha branch later when I have time (maybe tonight).

As noted by @seanbudd there is also the following indicating a problem with the installation of the voices.
However, this doesn't explain why 2022.1 no longer falls back to the French voice where 2021.3.4 does (or doesn't lang change in the first place).
These errors are not seen in the `OneCoreWithNVDA2021.3.4.log`.
Also relevant is that both runs of NVDA used the same config.

The warnings:
```
DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:50.999) - MainThread (27140):
Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_DavidM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:51.000) - MainThread (27140):
Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_ZiraM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:51.000) - MainThread (27140):
Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
```


In the `OneCoreWithNVDA2022.1.log` file (link in description)
The errors seem to be:
```
Input: kb(desktop):downArrow
IO - speech.speech.speak (15:50:55.623) - MainThread (27140):
Speaking [LangChangeCommand ('en'), 'world']
WARNING - speech.manager.SpeechManager._processSpeechSequence (15:50:55.623) - MainThread (27140):
Language en not supported by oneCore ({'fr_FR', 'de_DE'})
ERROR - NVDAHelperLocal (15:50:55.628) - Dummy-1 (24400):
Thread 24400, build\x86\localWin10\oneCoreSpeech.cpp, speak, 345:
Error -2147024894: Le fichier spécifié est introuvable.
ERROR - synthDrivers.oneCore.OneCoreSynthDriver._handleSpeechFailure (15:50:55.628) - Dummy-1 (24400):
OneCore synthesizer failed to speak
```

Note, this message `Language en not supported by oneCore ({'fr_FR', 'de_DE'})` detects that the language is not supported, and continues. The language could be removed:
In `source/speech/manager.py:342` method `_processSpeechSequence`
``` python
				if not langSupported:
					log.warning(f"Language {command.lang} not supported by {currentSynth.name} ({currentSynthLanguages})")
```

The most recent related change was 8c8e2bb03bfe46730b47e6cac908c3a1aebb6165

Then we can look at code histories for where LangComands are created.


I think we can rule out the lang code used in the `LangChangeCommands` as being the cause of the problem. The  `OneCoreWithNVDA2021.3.4.log` also shows the `LangChangeCommand ('en')` item sent to the synth, and a warning about it not being supported.

```
IO - speech.speech.speak (15:49:06.124) - MainThread (23516):
Speaking [LangChangeCommand ('fr_FR'), 'Hello']
IO - inputCore.InputManager.executeGesture (15:49:06.988) - winInputHook (19148):
Input: kb(desktop):downArrow
IO - speech.speech.speak (15:49:06.993) - MainThread (23516):
Speaking [LangChangeCommand ('en'), 'world']
WARNING - speech.manager.SpeechManager._processSpeechSequence (15:49:06.994) - MainThread (23516):
Language en not supported by oneCore ({'fr_FR', 'de_DE'})
IO - inputCore.InputManager.executeGesture (15:49:14.055) - winInputHook (19148):
Input: kb(desktop):NVDA+f1
Note however that in `OneCoreWithNVDA2021.3.4.log` there is not the `Error -2147024894: Le fichier spécifié est introuvable.` logged from `onecore.cpp` even though it looks like it should be logged (looking at version f25f33d3) of the file.
```c++
try {
		speakTask = create_task(instance->synth->SynthesizeSsmlToStreamAsync(textStr));
	} catch (Platform::Exception ^e) {
		LOG_ERROR(L"Error " << e->HResult << L": " << e->Message->Data());
		instance->callback(NULL, 0, NULL);
		return;
	}
```

This indicates to me that in `OneCoreWithNVDA2021.3.4.log`, the unsupported language code was not sent `ocSpeech_speak`.

The other change in this area is in `_getDefaultVoice`  (8c8e2bb03bfe46730b47e6cac908c3a1aebb6165) but I don't see an obvious problem with the implementation or usage.

@CyrilleB79 -
Would you be able to follow the STR with these builds, and upload the logs:
- https://ci.appveyor.com/api/buildjobs/pdcnnkr5k364c1hp/artifacts/output%2Fnvda_snapshot_try-Log2021.3.5-OneCoreLangSwitching-25507%2Cc11cbfc2.exe
- https://ci.appveyor.com/api/buildjobs/xm7o8e0o81cycyav/artifacts/output%2Fnvda_snapshot_try-Log2022.1beta1-OneCoreLangSwitching-25508%2C9982b5b8.exe
So we can reproduce the problem, could you export some information on your OneCore installation.

Open the Registry Editor
1. Navigate to the following keys
   - `HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4`
   - `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\`
1. For each key, export the selected branch and upload the .reg file via `File menu, Export`. 


Open Command Prompt
1. Navigate to the following directory: `cd %windir%\Speech_OneCore\Engines\TTS`
1. Get a list of all the files and folders in the directory: `dir /s /b /o:gn`
1. Upload the output
Here is the information regarding my OneCore installation:
[OneCoreInfo.zip](https://github.com/nvaccess/nvda/files/8848147/OneCoreInfo.zip)

Logs coming soon.

And here are the logs:
* With nvda_snapshot_try-Log2021.3.5-OneCoreLangSwitching-25507,c11cbfc2: [try-log2021.3.5Test.log](https://github.com/nvaccess/nvda/files/8848361/try-log2021.3.5Test.log)
  And the issue can be reproduced.
* With nvda_snapshot_try-Log2022.1beta1-OneCoreLangSwitching-25508,9982b5b8: [try-log2022.1beta1Test.log](https://github.com/nvaccess/nvda/files/8848362/try-log2022.1beta1Test.log)
  But the issue cannot be reproduced.

However, in both cases, many muche more errors in the log. I do not know if these logs will be of any use.

Regarding the files however, you may reproduce my OneCore config. Seems a broken installation or uninstallation of English voices.
@CyrilleB79 - thank you very much. Both the logs and the OneCore installation were helpful in determining the cause.

From what I understand, running NVDA from portable avoids this issue and this is not a regression.

OneCore keeps specific registry caches of OneCore for AT applications. NVDA's OneCore cache is: `HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4`.
The caches contain a subtree which is meant to mirror the path `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\*`.

For example `HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM` refers to `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM`.

To reproduce the issue, a user must:
1. install a OneCore voice
1. run an installed copy of NVDA using that voice - this creates the registry cache.
1. uninstall the OneCore voice
1. Note that the cached path still exists for the NVDA registry tree

NVDA behaviour for an installed copy:
1. When starting NVDA with OneCore, NVDA requests AllVoices from OneCore: https://github.com/nvaccess/nvda/blob/67bb085f67bf16467ce1ccb4ff263dc4b0caa9e9/nvdaHelper/localWin10/oneCoreSpeech.cpp#L410
1. OneCore returns all the Voices in the tree `HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4`
1.  NVDA checks if all voices are valid by trying to open the registry key. This fails logging the following:
    ```py
    DEBUGWARNING - synthDrivers.oneCore.OneCoreSynthDriver._isVoiceValid (15:50:50.999) - MainThread (27140):
    Could not open registry key HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_DavidM, FileNotFoundError(2, 'Le fichier spécifié est introuvable', None, 2, None)
    ```
1. The user continues to use NVDA
1. Language switching occurs, where speech attempts to switch to an uninstalled voice
1. OneCore fails to resolve the registry key from NVDA cache, and speech fails.
    ```py
    ERROR - NVDAHelperLocal (15:50:55.628) - Dummy-1 (24400):
    Thread 24400, build\x86\localWin10\oneCoreSpeech.cpp, speak, 345:
    Error -2147024894: Le fichier spécifié est introuvable.
    ERROR - synthDrivers.oneCore.OneCoreSynthDriver._handleSpeechFailure (15:50:55.628) - Dummy-1 (24400):
    OneCore synthesizer failed to speak
    ```

NVDA behaviour for a portable copy:
1. When starting NVDA with OneCore, NVDA requests AllVoices from OneCore.
1. OneCore returns all the Voices in the tree `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore`
1. NVDA checks if all voices are valid by trying to open the registry key. This succeeds as the NVDA cache is bypassed.
1. The user continues to use NVDA
1. Language switching occurs, where speech attempts to switch to an uninstalled voice
1. OneCore reverts to the default voice as the NVDA cache is bypassed.

Unfortunately, the resolution happens in the OneCore dll, and as such, I am uncertain on how to bypass the NVDA cache programmatically. 
I think we need to report this issue to Microsoft. OneCore should either remove the cache after uninstalling voices, or handle the resolution by falling back to the default voice if a cached voice is uninstalled.
What real purpose does the cache serve?

What is the result of deleting the tree at 
HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4

Will it disrupt the current voice in use?

Will OneCore simply recreate the tree with more accurate data?

>What real purpose does the cache serve?

I don't know, and I have struggled to find any documentation on this. My guess is application specific configuration (e.g. rate/volume) being cached for each voice. If there was any documentation on this, I would be inclined to delete/clean it as part of the registry fixing tool.

>Will it disrupt the current voice in use?

Not sure, I would assume it wouldn't disrupt.

>Will OneCore simply recreate the tree with more accurate data?

Yes, I believe so. 
@seanbudd, you write:
> From what I understand, running NVDA from portable avoids this issue and this is not a regression.

It seems that it is not completely true.
As I wrote earlier, using portable version (or more precisely temporary version run from the installer):
* With nvda_snapshot_try-Log2021.3.5-OneCoreLangSwitching-25507,c11cbfc2: the issue IS reproduced.
* With nvda_snapshot_try-Log2022.1beta1-OneCoreLangSwitching-25508,9982b5b8: the issue CANNOT BE reproduced.
But it's true that it is not a regression.
If you expect that the issue be reproduced on both 2021.3.5 and 2022.1 based portable versions, I will double check and re-test it, also with standard builds of these releases.

> 
@CyrilleB79 - ignore the logging builds as they introduce a change of behaviour.
Please test 2022.1 as a portable version - you will notice that you cannot reproduce the issue.
If you test 2021.3.5 as an installed copy - you will reproduce the issue.
```
WARNING - speech.manager.SpeechManager._processSpeechSequence (15:50:55.623) - MainThread (27140):
Language en not supported by oneCore ({'fr_FR', 'de_DE'})
```
Assuming that all these logs have this message, then I would suggest this is where we fix this. I'd suggest dropping the `LangChangeCommand` if it isn't supported.

I wouldn't mess with the registry settings, since they aren't part of our system. I would assume that they are preserved to preserve application specific settings in case the voice is reinstalled. For this reason, the preservation of the registry settings might not be considered a bug. Either way, we have a mechanism for getting the available voices, directly from the OneCore system.

Note, the registry checks (inside `def _isVoiceValid(self, id):`) were introduced with the initial support in #7110 there isn't further explanation about why they are required precisely. The issue https://github.com/nvaccess/nvda/issues/6159 also provides some discussion about this.
Certainly it would also be worth adding a note (and example) to the doc string that the registry paths vary between installed and non-installed NVDA instances.
Hello

I have performed more exhaustive testings with the same folder used as config folder (with the `-c` option) for all the tests. I have got the following results:
* NVDA 2021.3.5 from launcher: Error
* NVDA 2022.1 from launcher: Error
* NVDA 2021.3.5 portable copy: OK
* NVDA 2022.1 portable copy: OK
* NVDA 2021.3.5 installed: Not tested
* NVDA 2022.1 installed: Error

Thus, I confirm that there is no regression for 2022.1.

And it seems that the error is seen with NVDA installed or run from the launcher, but not with NVDA portable. This is quite strange because I would have expected that the launcher behaves as a portable copy since it creates one in the temp folder. Obviously, this is not the case.




Resolving patch:
diff --git a/source/speech/manager.py b/source/speech/manager.py
--- a/source/speech/manager.py
+++ b/source/speech/manager.py
@@ -331,15 +331,6 @@ def _processSpeechSequence(self, inSeq: SpeechSequence):
 
 		outSeq = []
 		for command in inSeq:
-			if isinstance(command, LangChangeCommand) and currentSynth.name == 'oneCore':
-				langCode = command.lang.split('_')[0]
-				langSupported = False
-				currentSynthLanguages = currentSynth.availableLanguages
-				for lang in currentSynthLanguages:
-					if lang and normalizeLanguage(lang).split('_')[0] == langCode:
-						langSupported = True
-				if not langSupported:
-					log.warning(f"Language {command.lang} not supported by {currentSynth.name} ({currentSynthLanguages})")
 			if isinstance(command, BaseCallbackCommand):
 				# When the synth reaches this point, we want to call the callback.
 				speechIndex = next(self._indexCounter)
diff --git a/source/speechXml.py b/source/speechXml.py
--- a/source/speechXml.py
+++ b/source/speechXml.py
@@ -1,5 +1,5 @@
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2016-2021 NV Access Limited
+# Copyright (C) 2016-2022 NV Access Limited
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
@@ -14,7 +14,7 @@
 import re
 import speech
 import textUtils
-from speech.commands import SpeechCommand
+from speech.commands import LangChangeCommand, SpeechCommand
 from logHandler import log
 
 XML_ESCAPES = {
@@ -49,7 +49,8 @@ def _buildInvalidXmlRegexp():
 RE_INVALID_XML_CHARS = _buildInvalidXmlRegexp()
 REPLACEMENT_CHAR = textUtils.REPLACEMENT_CHAR
 
-def toXmlLang(nvdaLang):
+
+def toXmlLang(nvdaLang: str) -> str:
 	"""Convert an NVDA language to an XML language.
 	"""
 	return nvdaLang.replace("_", "-")
@@ -153,7 +154,7 @@ def _outputTags(self):
 			self._openTags.append(tag)
 		self._tagsChanged = False
 
-	def generateXml(self, commands):
+	def generateXml(self, commands) -> str:
 		"""Generate XML from a sequence of balancer commands and text.
 		"""
 		for command in commands:
@@ -235,7 +236,7 @@ class SsmlConverter(SpeechXmlConverter):
 	"""Converts an NVDA speech sequence to SSML.
 	"""
 
-	def __init__(self, defaultLanguage):
+	def __init__(self, defaultLanguage: str):
 		self.defaultLanguage = toXmlLang(defaultLanguage)
 
 	def generateBalancerCommands(self, speechSequence):
@@ -254,7 +255,7 @@ def convertCharacterModeCommand(self, command):
 		else:
 			return StopEnclosingTextCommand()
 
-	def convertLangChangeCommand(self, command):
+	def convertLangChangeCommand(self, command: LangChangeCommand) -> SetAttrCommand:
 		lang = command.lang or self.defaultLanguage
 		lang = toXmlLang(lang)
 		return SetAttrCommand("voice", "xml:lang", lang)
diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py
--- a/source/synthDrivers/oneCore.py
+++ b/source/synthDrivers/oneCore.py
@@ -7,7 +7,16 @@
 """
 
 import os
-import sys
+from typing import (
+	Any,
+	Callable,
+	Generator,
+	List,
+	Optional,
+	Set,
+	Tuple,
+	Union
+)
 from collections import OrderedDict
 import ctypes
 import winreg
@@ -25,6 +34,7 @@
 import config
 import nvwave
 import queueHandler
+from speech.types import SpeechSequence
 import speech
 import speechXml
 import languageHandler
@@ -47,6 +57,24 @@
 ocSpeech_Callback = ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p)
 
 class _OcSsmlConverter(speechXml.SsmlConverter):
+	def __init__(
+			self,
+			defaultLanguage: str,
+			availableLanguages: Set[str],
+	):
+		"""
+		Used for newer OneCore installations (OneCore API > 5)
+		where supportsProsodyOptions is True.
+		This allows for changing rate, volume and pitch after initialization.
+
+		@param defaultLanguage: language with locale, installed by OneCore (e.g. 'en_US')
+		@param availableLanguages: languages with locale, installed by OneCore (e.g. 'zh_HK', 'en_US')
+		"""
+		self.lowerCaseAvailableLanguages = {language.lower() for language in availableLanguages}
+		self.availableLanguagesWithoutLocale = {
+			language.split("_")[0] for language in self.lowerCaseAvailableLanguages
+		}
+		super().__init__(defaultLanguage)
 
 	def _convertProsody(self, command, attr, default, base=None):
 		if base is None:
@@ -74,23 +102,52 @@ def convertCharacterModeCommand(self, command):
 		# Therefore, we don't use it.
 		return None
 
-	def convertLangChangeCommand(self, command):
+	def convertLangChangeCommand(self, command: LangChangeCommand) -> Optional[speechXml.SetAttrCommand]:
 		lcid = languageHandler.localeNameToWindowsLCID(command.lang)
 		if lcid is languageHandler.LCID_NONE:
 			log.debugWarning(f"Invalid language: {command.lang}")
 			return None
+
+		normalizedLanguage = command.lang.lower().replace("-", "_")
+		normalizedLanguageWithoutLocale = normalizedLanguage.split("_")[0]
+		if (
+			normalizedLanguage not in self.lowerCaseAvailableLanguages
+			and normalizedLanguageWithoutLocale not in self.availableLanguagesWithoutLocale
+		):
+			log.warning(f"Language {command.lang} not supported ({self.lowerCaseAvailableLanguages})")
+			return None
+
 		return super().convertLangChangeCommand(command)
 
 class _OcPreAPI5SsmlConverter(_OcSsmlConverter):
 
-	def __init__(self, defaultLanguage, rate, pitch, volume):
-		super(_OcPreAPI5SsmlConverter, self).__init__(defaultLanguage)
+	def __init__(
+			self,
+			defaultLanguage: str,
+			availableLanguages: Set[str],
+			rate: float,
+			pitch: float,
+			volume: float,
+	):
+		"""
+		Used for older OneCore installations (OneCore API < 5),
+		where supportsProsodyOptions is False.
+		This means we must initially set a good default for rate, volume and pitch,
+		as this can't be changed after initialization.
+
+		@param defaultLanguage: language with locale, installed by OneCore (e.g. 'en_US')
+		@param availableLanguages: languages with locale, installed by OneCore (e.g. 'zh_HK', 'en_US')
+		@param rate: from 0-100
+		@param pitch: from 0-100
+		@param volume: from 0-100
+		"""
+		super().__init__(defaultLanguage, availableLanguages)
 		self._rate = rate
 		self._pitch = pitch
 		self._volume = volume
 
-	def generateBalancerCommands(self, speechSequence):
-		commands = super(_OcPreAPI5SsmlConverter, self).generateBalancerCommands(speechSequence)
+	def generateBalancerCommands(self, speechSequence: SpeechSequence) -> Generator[Any, None, None]:
+		commands = super().generateBalancerCommands(speechSequence)
 		# The EncloseAllCommand from SSML must be first.
 		yield next(commands)
 		# OneCore didn't provide a way to set base prosody values before API version 5.
@@ -177,12 +234,13 @@ def __init__(self):
 
 		self._earlyExitCB = False
 		self._callbackInst = ocSpeech_Callback(self._callback)
-		self._ocSpeechToken = self._dll.ocSpeech_initialize(self._callbackInst)
+		self._ocSpeechToken: Optional[ctypes.POINTER] = self._dll.ocSpeech_initialize(self._callbackInst)
 		self._dll.ocSpeech_getVoices.restype = NVDAHelper.bstrReturn
 		self._dll.ocSpeech_getCurrentVoiceId.restype = ctypes.c_wchar_p
 		self._player= None
 		# Initialize state.
-		self._queuedSpeech = []
+		self._queuedSpeech: List[Union[str, Tuple[Callable[[ctypes.POINTER, float], None], float]]] = []
+
 		self._wasCancelled = False
 		self._isProcessing = False
 		# Initialize the voice to a sane default
@@ -235,11 +293,17 @@ def cancel(self):
 		if self._player:
 			self._player.stop()
 
-	def speak(self, speechSequence):
+	def speak(self, speechSequence: SpeechSequence) -> None:
 		if self.supportsProsodyOptions:
-			conv = _OcSsmlConverter(self.language)
+			conv = _OcSsmlConverter(self.language, self.availableLanguages)
 		else:
-			conv = _OcPreAPI5SsmlConverter(self.language, self._rate, self._pitch, self._volume)
+			conv = _OcPreAPI5SsmlConverter(
+				self.language,
+				self.availableLanguages,
+				self._rate,
+				self._pitch,
+				self._volume
+			)
 		text = conv.convertToXml(speechSequence)
 		# #7495: Calling WaveOutOpen blocks for ~100 ms if called from the callback
 		# when the SSML includes marks.
@@ -249,7 +313,7 @@ def speak(self, speechSequence):
 			self._player.open()
 		self._queueSpeech(text)
 
-	def _queueSpeech(self, item):
+	def _queueSpeech(self, item: str) -> None:
 		self._queuedSpeech.append(item)
 		# We only process the queue here if it isn't already being processed.
 		if not self._isProcessing:
@@ -432,8 +496,9 @@ def _getVoiceInfoFromOnecoreVoiceString(self, voiceStr):
 
 	def _getAvailableVoices(self):
 		voices = OrderedDict()
-		# Fetch the full list of voices that Onecore speech knows about.
-		# Note that it may give back voices that are uninstalled or broken. 
+		# Fetch the full list of voices that OneCore speech knows about.
+		# Note that it may give back voices that are uninstalled or broken.
+		# Refer to _isVoiceValid for information on uninstalled or broken voices.
 		voicesStr = self._dll.ocSpeech_getVoices(self._ocSpeechToken).split('|')
 		for index,voiceStr in enumerate(voicesStr):
 			voiceInfo=self._getVoiceInfoFromOnecoreVoiceString(voiceStr)
@@ -444,14 +509,33 @@ def _getAvailableVoices(self):
 			voices[voiceInfo.id] =  voiceInfo
 		return voices
 
-	def _isVoiceValid(self,ID):
-		"""
+	def _isVoiceValid(self, ID: str) -> bool:
+		r"""
 		Checks that the given voice actually exists and is valid.
 		It checks the Registry, and also ensures that its data files actually exist on this machine.
 		@param ID: the ID of the requested voice.
-		@type ID: string
-		@returns: True if the voice is valid, false otherwise.
-		@rtype: boolean
+		@returns: True if the voice is valid, False otherwise.
+
+		OneCore keeps specific registry caches of OneCore for AT applications.
+		Installed copies of NVDA have a OneCore cache in:
+		`HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4`.
+		The caches contain a subtree which is meant to mirror the path:
+		`HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\*`.
+
+		For example:
+		`HKEY_CURRENT_USER\Software\Microsoft\Speech_OneCore\Isolated\Ny37kw9G-o42UiJ1z6Qc_sszEKkCNywTlrTOG0QKVB4\
+		HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM`
+		refers to `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enUS_MarkM`.
+
+		Languages which have been used by an installed copy of NVDA,
+		but uninstalled from the system are kept in the cache.
+		For installed copies of NVDA, OneCore will still attempt to use these languages,
+		so we must check if they are valid first.
+		For portable copies, the cache is bypassed and `HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\`
+		is read directly.
+
+		For more information, refer to:
+		https://github.com/nvaccess/nvda/issues/13732#issuecomment-1149386711
 		"""
 		IDParts = ID.split('\\')
 		rootKey = getattr(winreg, IDParts[0])


Test patch: