Repository: nvaccess/nvda

Issue:
Unable to load the oneCore synthesizer
It is impossible to switch to the oneCore synthesizer from nvda's synthesizer settings dialogue.
When I choose oneCore synthesizer from the list and I press OK, a message comes up saying could not load the oneCore synthesizer and there is only an OK button to press in this dialogue.
I find this very strange since narrator works fine and it is using the same voices so I don't know why it doesn't work with nvda.

I am using windows 10 with the latest updates and the latest nvda master snapshot.

All the information is included in the attached log.
Nikos
[nvda.txt](https://github.com/nvaccess/nvda/files/1270002/nvda.txt)




Discussion:
Hi, oh great, newline problem… CC @MichaelDCurran

Edit: removed original email
I cannot reproduce it on my system.

I have all voices wich i need.


W dniu 01.09.2017 o 18:16, Joseph Lee pisze:
> Hi, oh great, newline problem… CC @MichaelDCurran
>
>
>
> From: nikosdemetriou [mailto:notifications@github.com]
> Sent: Friday, September 1, 2017 4:04 AM
> To: nvaccess/nvda <nvda@noreply.github.com>
> Cc: Subscribed <subscribed@noreply.github.com>
> Subject: [nvaccess/nvda] Unable to load the oneCore synthesizer (#7553)
>
>
>
> It is impossible to switch to the oneCore synthesizer from nvda's 
> synthesizer settings dialogue.
> When I choose oneCore synthesizer from the list and I press OK, a 
> message comes up saying could not load the oneCore synthesizer and 
> there is only an OK button to press in this dialogue.
>
> I am using windows 10 with the latest updates and the latest nvda 
> master snapshot.
>
> All the information is included in the attached log.
> Nikos
> nvda.txt <https://github.com/nvaccess/nvda/files/1270002/nvda.txt>
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub 
> <https://github.com/nvaccess/nvda/issues/7553> , or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/AHgLkDL3jH4Q1uKSyiBF8gK5hiHDqkAEks5sd-SagaJpZM4PKAtb> 
> .
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub 
> <https://github.com/nvaccess/nvda/issues/7553#issuecomment-326622306>, 
> or mute the thread 
> <https://github.com/notifications/unsubscribe-auth/AKohkz8LIuG6xMR9jtydBvGVaiI1HknDks5seC3jgaJpZM4PKAtb>.
>


I also have the same issue on Windows 10 (build 17046), here is my log output:

```
WARNING - synthDriverHandler.SynthDriver.loadSettings (16:52:58.943):
Invalid voice: HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech_OneCore\Voices\Tokens\MSTTS_V110_enGB_HazelM
ERROR - synthDriverHandler.setSynth (16:52:58.957):
setSynth
Traceback (most recent call last):
  File "synthDriverHandler.pyo", line 89, in setSynth
  File "synthDriverHandler.pyo", line 498, in loadSettings
  File "synthDriverHandler.pyo", line 36, in changeVoice
  File "speechDictHandler\__init__.pyo", line 128, in loadVoiceDict
KeyError: u'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices\\Tokens\\MSTTS_V110_enGB_HazelM'
INFO - synthDriverHandler.setSynth (16:52:58.963):
Loaded synthDriver sapi5
```
Hi, this is commonly caused by the fact that the voice you used before isn’t there anymore. Thanks.

Edit: removed original email
@josephsl Other than re-installing the missing voice, is there anything that can be done to make the driver fall back to whatever voice currently installed?
We could potentially check if the voice is available (see `def setSynth` of `source/synthDriverHandler.py`) and if not choose an arbitrary voice (eg the first available).

The ramifications of this is that from the users perspective, the voice will no longer be what they are familiar with, and they will not know why. Though, this does not seem worse than the synth changing with no explanation.
Setting this as priority 2. It looks like it should be fairly quick to fix.

Resolving patch:
diff --git a/source/languageHandler.py b/source/languageHandler.py
--- a/source/languageHandler.py
+++ b/source/languageHandler.py
@@ -137,27 +137,36 @@ def pgettext(context, message):
 			return unicode(message)
 	return pgettext
 
+def getWindowsLanguage():
+	"""
+	Fetches the locale name of the user's configured language in Windows.
+	"""
+	windowsLCID=ctypes.windll.kernel32.GetUserDefaultUILanguage()
+	try:
+		localeName=locale.windows_locale[windowsLCID]
+	except KeyError:
+		# #4203: some locale identifiers from Windows 8 don't exist in Python's list.
+		# Therefore use windows' own function to get the locale name.
+		# Eventually this should probably be used all the time.
+		bufSize=32
+		buf=ctypes.create_unicode_buffer(bufSize)
+		dwFlags=0
+		try:
+			ctypes.windll.kernel32.LCIDToLocaleName(windowsLCID,buf,bufSize,dwFlags)
+		except AttributeError:
+			pass
+		localeName=buf.value
+		if localeName:
+			localeName=normalizeLanguage(localeName)
+		else:
+			localeName="en"
+	return localeName
+
 def setLanguage(lang):
 	global curLang
 	try:
 		if lang=="Windows":
-			windowsLCID=ctypes.windll.kernel32.GetUserDefaultUILanguage()
-			try:
-				localeName=locale.windows_locale[windowsLCID]
-			except KeyError:
-				# #4203: some locale identifiers from Windows 8 don't exist in Python's list.
-				# Therefore use window' own function to get the locale name.
-				# Eventually this should probably be used all the time.
-				buf=ctypes.create_unicode_buffer(32)
-				try:
-					ctypes.windll.kernel32.LCIDToLocaleName(windowsLCID,buf,32,0)
-				except AttributeError:
-					pass
-				localeName=buf.value
-				if localeName:
-					localeName=normalizeLanguage(localeName)
-				else:
-					localeName="en"
+			localeName=getWindowsLanguage()
 			trans=gettext.translation('nvda',localedir='locale',languages=[localeName])
 			curLang=localeName
 		else:
diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py
--- a/source/synthDrivers/oneCore.py
+++ b/source/synthDrivers/oneCore.py
@@ -118,6 +118,8 @@ def __init__(self):
 		self._queuedSpeech = []
 		self._wasCancelled = False
 		self._isProcessing = False
+		# Initialize the voice to a sane default
+		self.voice=self._getDefaultVoice()
 		# Set initial values for parameters that can't be queried.
 		# This initialises our cache for the value.
 		self.rate = 50
@@ -221,24 +223,45 @@ def _callback(self, bytes, len, markers):
 			log.debug("Done pushing audio")
 		self._processQueue()
 
-	def _getAvailableVoices(self, onlyValid=True):
+	def _getVoiceInfoFromOnecoreVoiceString(self, voiceStr):
+		"""
+		Produces an NVDA VoiceInfo object representing the given voice string from Onecore speech.
+		"""
+		# The voice string is made up of the ID, the language, and the display name.
+		ID,language,name=voiceStr.split(':')
+		language=language.replace('-','_')
+		return VoiceInfo(ID,name,language=language)
+
+	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. 
 		voicesStr = self._dll.ocSpeech_getVoices(self._handle).split('|')
-		for voiceStr in voicesStr:
-			id, name = voiceStr.split(":")
-			if onlyValid and not self._isVoiceValid(id):
+		for index,voiceStr in enumerate(voicesStr):
+			voiceInfo=self._getVoiceInfoFromOnecoreVoiceString(voiceStr)
+			# Filter out any invalid voices.
+			if not self._isVoiceValid(voiceInfo.ID):
 				continue
-			voices[id] = VoiceInfo(id, name)
+			voiceInfo.onecoreIndex=index
+			voices[voiceInfo.ID] =  voiceInfo
 		return voices
 
-	def _isVoiceValid(self, id):
-		idParts = id.split('\\')
-		rootKey = getattr(_winreg, idParts[0])
-		subkey = "\\".join(idParts[1:])
+	def _isVoiceValid(self,ID):
+		"""
+		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
+		"""
+		IDParts = ID.split('\\')
+		rootKey = getattr(_winreg, IDParts[0])
+		subkey = "\\".join(IDParts[1:])
 		try:
 			hkey = _winreg.OpenKey(rootKey, subkey)
 		except WindowsError as e:
-			log.debugWarning("Could not open registry key %s, %s" % (id, e))
+			log.debugWarning("Could not open registry key %s, %s" % (ID, e))
 			return False
 		try:
 			langDataPath = _winreg.QueryValueEx(hkey, 'langDataPath')
@@ -267,14 +290,39 @@ def _isVoiceValid(self, id):
 	def _get_voice(self):
 		return self._dll.ocSpeech_getCurrentVoiceId(self._handle)
 
-	def _set_voice(self, id):
-		voices = self._getAvailableVoices(onlyValid=False)
-		for index, voice in enumerate(voices):
-			if voice == id:
-				break
-		else:
-			raise LookupError("No such voice: %s" % id)
-		self._dll.ocSpeech_setVoice(self._handle, index)
+	def _set_voice(self, ID):
+		voices = self.availableVoices
+		# Try setting the requested voice
+		for voice in voices.itervalues():
+			if voice.ID == ID:
+				self._dll.ocSpeech_setVoice(self._handle, voice.onecoreIndex)
+				return
+		raise LookupError("No such voice: %s"%ID)
+
+	def _getDefaultVoice(self):
+		"""
+		Finds the best available voice that can be used as a default.
+		It first tries finding a voice with the same language and country as the user's configured Windows language (E.g. en_AU), 
+		else one that matches just the language (E.g. en), 
+		else simply the first available.
+		@returns: the ID of the voice, suitable for passing to self.voice for setting.
+		@rtype: string
+		"""
+		voices = self.availableVoices
+		# Try matching to NVDA language
+		fullLanguage=languageHandler.getWindowsLanguage()
+		for voice in voices.itervalues():
+			if voice.language==fullLanguage:
+				return voice.ID
+		baseLanguage=fullLanguage.split('_')[0]
+		if baseLanguage!=fullLanguage:
+			for voice in voices.itervalues():
+				if voice.language.startswith(baseLanguage):
+					return voice.ID
+		# Just use the first available
+		for voice in voices.itervalues():
+			return voice.ID
+		raise RuntimeError("No voices available")
 
 	def _get_language(self):
 		return self._dll.ocSpeech_getCurrentVoiceLanguage(self._handle)


Test patch: