Repository: nvaccess/nvda

Issue:
NVDA does not save One core voices settings with Rate boost on
With new rate boost option introduced in the newest Alpha releases NVDA is not able to save the configuration for One core voices while the rateboost is on. When changes for rate and pitch are made for One core voices, it is possible to save the settings, but after restarting NVDA or after switching to another synth and going back to One core voices, the settings go to their default values. However if rate boost for One core voices is turned off, than all the settings are saved and reloaded correctly.
It is happening on Windows 10 1809 with the last Alpha release alpha-17205,93ccd518

nvda threshold does not advertise and makes an error sound when pruning capital letter

### Steps to reproduce:
Open Notepad.
Please enter a capital letter.
Now navigate left with the arrow and try to read the character in uppercase.
### Actual behavior:
When typing an uppercase letter or trying to read an uppercase letter, nvda makes the error tone and does not speak the uppercase character.
### Expected behavior:
nvda should read the uppercase characters normally, either when navigating character by character, or when typing a capital character.
### System configuration
using the windows voice one core in Portuguese Brazil, voice Maria.
#### NVDA installed/portable/running from source:
install
#### NVDA version:
threshold 17249
#### Windows version:
10 17763.475
#### Name and version of other software in use when reproducing the issue:

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your PC?
yes
#### Have you tried any other versions of NVDA? If so, please report their behaviors.
yes
alpha versions of nvda do not have this problem
[capital.txt](https://github.com/nvaccess/nvda/files/3187265/capital.txt)



Discussion:
I can't reproduce this for changing the pitch, but for rate, I certainly can. I will dive into this.


Resolving patch:
diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py
--- a/source/synthDrivers/oneCore.py
+++ b/source/synthDrivers/oneCore.py
@@ -33,7 +33,9 @@
 
 class _OcSsmlConverter(speechXml.SsmlConverter):
 
-	def _convertProsody(self, command, attr, default, base):
+	def _convertProsody(self, command, attr, default, base=None):
+		if base is None:
+			base = default
 		if command.multiplier == 1 and base == default:
 			# Returning to synth default.
 			return speechXml.DelAttrCommand("prosody", attr)
@@ -44,11 +46,13 @@ def _convertProsody(self, command, attr, default, base):
 			return speechXml.SetAttrCommand("prosody", attr, "%d%%" % val)
 
 	def convertRateCommand(self, command):
-		return self._convertProsody(command, "rate", 50, self._rate)
+		return self._convertProsody(command, "rate", 50)
+
 	def convertPitchCommand(self, command):
-		return self._convertProsody(command, "pitch", 50, self._pitch)
+		return self._convertProsody(command, "pitch", 50)
+
 	def convertVolumeCommand(self, command):
-		return self._convertProsody(command, "volume", 100, self._volume)
+		return self._convertProsody(command, "volume", 100)
 
 	def convertCharacterModeCommand(self, command):
 		# OneCore's character speech sounds weird and doesn't support pitch alteration.
@@ -82,6 +86,15 @@ def generateBalancerCommands(self, speechSequence):
 		for command in commands:
 			yield command
 
+	def convertRateCommand(self, command):
+		return self._convertProsody(command, "rate", 50, self._rate)
+
+	def convertPitchCommand(self, command):
+		return self._convertProsody(command, "pitch", 50, self._pitch)
+
+	def convertVolumeCommand(self, command):
+		return self._convertProsody(command, "volume", 100, self._volume)
+
 class SynthDriver(SynthDriver):
 
 	MIN_PITCH = 0.0
@@ -125,17 +138,19 @@ def __init__(self):
 		super(SynthDriver, self).__init__()
 		self._dll = NVDAHelper.getHelperLocalWin10Dll()
 		self._dll.ocSpeech_getCurrentVoiceLanguage.restype = ctypes.c_wchar_p
+		# Set initial values for parameters that can't be queried when prosody is not supported.
+		# This initialises our cache for the value.
+		# When prosody is supported, the values are used for cachign reasons.
+		self._rate = 50
+		self._pitch = 50
+		self._volume = 100
+
 		if self.supportsProsodyOptions:
 			self._dll.ocSpeech_getPitch.restype = ctypes.c_double
 			self._dll.ocSpeech_getVolume.restype = ctypes.c_double
 			self._dll.ocSpeech_getRate.restype = ctypes.c_double
 		else:
 			log.debugWarning("Prosody options not supported")
-			# Set initial values for parameters that can't be queried.
-			# This initialises our cache for the value.
-			self._rate = 50
-			self._pitch = 50
-			self._volume = 100
 		self._handle = self._dll.ocSpeech_initialize()
 		self._callbackInst = ocSpeech_Callback(self._callback)
 		self._dll.ocSpeech_setCallback(self._handle, self._callbackInst)
@@ -220,8 +235,8 @@ def _get_pitch(self):
 		return self._paramToPercent(rawPitch, self.MIN_PITCH, self.MAX_PITCH)
 
 	def _set_pitch(self, pitch):
+		self._pitch = pitch
 		if not self.supportsProsodyOptions:
-			self._pitch = pitch
 			return
 		rawPitch = self._percentToParam(pitch, self.MIN_PITCH, self.MAX_PITCH)
 		self._queuedSpeech.append((self._dll.ocSpeech_setPitch, rawPitch))
@@ -233,8 +248,8 @@ def _get_volume(self):
 		return int(rawVolume * 100)
 
 	def _set_volume(self, volume):
+		self._volume = volume
 		if not self.supportsProsodyOptions:
-			self._volume = volume
 			return
 		rawVolume = volume / 100.0
 		self._queuedSpeech.append((self._dll.ocSpeech_setVolume, rawVolume))
@@ -247,8 +262,8 @@ def _get_rate(self):
 		return self._paramToPercent(rawRate, self.MIN_RATE, maxRate)
 
 	def _set_rate(self, rate):
+		self._rate = rate
 		if not self.supportsProsodyOptions:
-			self._rate = rate
 			return
 		maxRate = self.BOOSTED_MAX_RATE if self._rateBoost else self.DEFAULT_MAX_RATE
 		rawRate = self._percentToParam(rate, self.MIN_RATE, maxRate)
@@ -262,7 +277,9 @@ def _get_rateBoost(self):
 	def _set_rateBoost(self, enable):
 		if enable == self._rateBoost:
 			return
-		rate = self.rate
+		# Use the cached rate to calculate the new rate with rate boost enabled.
+		# If we don't, getting the rate property will return the default rate when initializing the driver and applying settings.
+		rate = self._rate
 		self._rateBoost = enable
 		self.rate = rate
 


Test patch: