Repository: nvaccess/nvda

Issue:
Threshold: Couldn't load SAPI5 synthesizer due to wave out not hookable
### Steps to reproduce:
This can also be reproduced using the gui, but using the python console is more straight forward.

1. Launch a Python console
2. Paste in the following

```
from synthDrivers.sapi5 import SynthDriver
SynthDriver()

```

### Actual behavior:
The following error in de log:

```
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "synthDrivers\sapi5.pyc", line 134, in __init__
  File "synthDrivers\sapi5.pyc", line 74, in ensureWaveOutHooks
  File "synthDrivers\sapi5.pyc", line 40, in __init__
RuntimeError: could not hook waveOutOpen
```

### Expected behavior:
WHen enabling sapi5 from the gui, speech works as normal.

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
threshold-18146

#### Windows version:
Windows 10 1903

#### Name and version of other software in use when reproducing the issue:
Microsoft SAPI5 voices

#### 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, a portable version of ALPHA works fine.


Discussion:
Interesting enough, running threshold from source with exactly the same config loads sapi5 just fine.

Resolving patch:
diff --git a/source/synthDrivers/sapi5.py b/source/synthDrivers/sapi5.py
--- a/source/synthDrivers/sapi5.py
+++ b/source/synthDrivers/sapi5.py
@@ -22,6 +22,7 @@
 import config
 import nvwave
 from logHandler import log
+import weakref
 
 # SPAudioState enumeration
 SPAS_CLOSED=0
@@ -31,16 +32,33 @@
 
 class FunctionHooker(object):
 
-	def __init__(self,targetDll,importDll,funcName,newFunction):
-		hook=NVDAHelper.localLib.dllImportTableHooks_hookSingle(targetDll,importDll,funcName,newFunction)
-		if hook:
-			log.debug("hooked %s"%funcName)
+	def __init__(
+		self,
+		targetDll: str,
+		importDll: str,
+		funcName: str,
+		newFunction # result of ctypes.WINFUNCTYPE
+	):
+		# dllImportTableHooks_hookSingle expects byte strings.
+		try:
+			self._hook=NVDAHelper.localLib.dllImportTableHooks_hookSingle(
+				targetDll.encode("mbcs"),
+				importDll.encode("mbcs"),
+				funcName.encode("mbcs"),
+				newFunction
+			)
+		except UnicodeEncodeError:
+			log.error("Error encoding FunctionHooker input parameters", exc_info=True)
+			self._hook = None
+		if self._hook:
+			log.debug(f"Hooked {funcName}")
 		else:
-			log.debug("could not hook %s"%funcName)
-			raise RuntimeError("could not hook %s"%funcName)
+			log.error(f"Could not hook {funcName}")
+			raise RuntimeError(f"Could not hook {funcName}")
 
 	def __del__(self):
-		NVDAHelper.localLib.dllImportTableHooks_unhookSingle(self._hook)
+		if self._hook:
+			NVDAHelper.localLib.dllImportTableHooks_unhookSingle(self._hook)
 
 _duckersByHandle={}
 
@@ -88,13 +106,21 @@ class SapiSink(object):
 	"""
 
 	def __init__(self, synth):
-		self.synth = synth
+		self.synthRef = weakref.ref(synth)
 
 	def Bookmark(self, streamNum, pos, bookmark, bookmarkId):
-		synthIndexReached.notify(synth=self.synth, index=bookmarkId)
+		synth = self.synthRef()
+		if synth is None:
+			log.debugWarning("Called Bookmark method on SapiSink while driver is dead")
+			return
+		synthIndexReached.notify(synth=synth, index=bookmarkId)
 
 	def EndStream(self, streamNum, pos):
-		synthDoneSpeaking.notify(synth=self.synth)
+		synth = self.synthRef()
+		if synth is None:
+			log.debugWarning("Called Bookmark method on EndStream while driver is dead")
+			return
+		synthDoneSpeaking.notify(synth=synth)
 
 class SynthDriver(SynthDriver):
 	supportedSettings=(SynthDriver.VoiceSetting(),SynthDriver.RateSetting(),SynthDriver.PitchSetting(),SynthDriver.VolumeSetting())


Test patch: