Repository: nvaccess/nvda

Issue:
OneCore performance
<!--
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/wiki/Github-issue-template-explanation-and-examples
-->

### Steps to reproduce:
1. Switch to OneCore.
1. Press windows+m to go to the desktop.
1. Tab to the start button.
1. Shift+tab and tab a few times.

### Actual behavior:
The delay between Desktop list and the icon name varies between fast and slow.
### Expected behavior:
The delay should stay the same.
### System configuration
#### NVDA installed/portable/running from source:
Portable
#### NVDA version:
2019.3 Beta 2
#### Windows version:
Windows 10 build 1909
#### 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.
2019.2 works fine.



Discussion:
I've been seeing this on a client's I7 quadcore laptop, running 2019.2.1 under 
Win 10 1903.

She keeps complaining about the "lag", and when using remote to operate it, I 
can observe it directly. Actions happen, but One Core David takes a notable 
length of time before mentioning them.

It seems worse during some boot cycles than others.

I haven't been able to figure out if it is an actual problem, or just the 
general ridiculous slowness of One Core as contrasted with Espeak.

@feerrenrut, I mentioned briefly when we spoke today that I had a theory (more of a hunch) regarding the OneCore lag. It turns out my hunch was correct.

If you take a look at ffa71c7c (#7495), you'll note that at one point, we were getting lag when typing characters. I tracked this down to WaveOutOpen (called by nvwave) blocking for ~100 ms whenever it was called from the OneCore engine callback if there were SSML marks (indexes) in the stream. I was never able to work out why that happened; I just worked around it by ensuring we open the device before pushing to the synth, rather than opening the device in the callback upon receiving the first chunk of audio.

What's new in NVDA 2019.3 is that *all* utterances contain at least one SSML mark, not just characters and say all. Digging into this, we call WavePlayer.idle() when there's nothing more in the queue, which (synchronously) pushes the last chunk of audio and closes the device. Because that blocks, something can arrive in the queue after that point. This is fine - we handle it - except that what this means is that in this case, we'll end up calling WaveOutOpen from the callback for the next chunk of audio, thus triggering this ~100 ms lag before the next utterance is played.

I have a patch which fixes this... most of the time. Instead of calling WavePlayer.idle(), we call WavePlayer.sync(). If the queue is empty after that returns, only then do we call WavePlayer.idle() and allow the device to close. Unfortunately, there's still a rare race condition where as we're closing the device, something can arrive in the queue, so we hit the same issue again.

I can submit this PR, but it's not a complete fix. I think the more complete solution here is to make WavePlayer.idle() only close the device if there's no activity for, say, 10 or 15 seconds. Aside from completely fixing this issue, that would improve audio performance between utterances in general, as well as dealing with some irritating audio drivers/hardware which take a long time to open the device and/or truncate the start of the audio.

Resolving patch:
diff --git a/source/synthDrivers/oneCore.py b/source/synthDrivers/oneCore.py
--- a/source/synthDrivers/oneCore.py
+++ b/source/synthDrivers/oneCore.py
@@ -288,9 +288,19 @@ def _set_rateBoost(self, enable):
 
 	def _processQueue(self):
 		if not self._queuedSpeech:
-			# There are no more queued utterances at this point, so call idle.
+			# There are no more queued utterances at this point, so call sync.
 			# This blocks while waiting for the final chunk to play,
 			# so by the time this is done, there might be something queued.
+			# #10721: We use sync instead of idle because idle closes the audio
+			# device. If there's something in the queue after playing the final chunk,
+			# that will result in WaveOutOpen being called in the callback when we
+			# push the next chunk of audio. We *really* don't want this because calling
+			# WaveOutOpen blocks for ~100 ms if called from the callback when the SSML
+			# includes marks, resulting in lag between utterances.
+			log.debug("Calling sync on audio player")
+			self._player.sync()
+		if not self._queuedSpeech:
+			# There's still nothing in the queue, so it's okay to call idle now.
 			log.debug("Calling idle on audio player")
 			self._player.idle()
 			synthDoneSpeaking.notify(synth=self)


Test patch: