Make SAPI5 & MSSP voices use WavePlayer (WASAPI) - #17592
Conversation
LeonarddeR
left a comment
There was a problem hiding this comment.
This is marvelous work! Great job
seanbudd
left a comment
There was a problem hiding this comment.
Thanks @gexgd0419 ! Mostly minor review items
Co-authored-by: Sean Budd <seanbudd123@gmail.com>
|
When using windows 10 22h2, and finnish mikropuhe 5.1 I get following error: IO - speech.speech.speak (06:49:48.828) - MainThread (11288): Unfortunately this makes tts voice very stuttering. |
|
@burmancomp can you please open an issue with a full debug log? |
# Method implementations could check for and return E_POINTER
# themselves. Or an error will be raised when
# 'outargs[i][0] = value' is executed.
# for a in outargs:
# if not a:
# return E_POINTERI guess a null pointer was passed in as the But how should I check the out parameter when I'm using the high level implementation? |
|
I think that using the low level implementation is more appropriate here. Passing NULL to the out parameter is explicitly allowed, and it shouldn't return an error in this case. |
|
That sounds pretty reasonable to me. I'm pretty sure however that the error we're getting now comes from an exception handler in comtypes, I"m not sure how exception handling works in the low level implementation. We must avoid harsh crashes of NVDA. |
|
Seems that both def call_with_this(*args, **kw):
try:
result = mth(*args, **kw)
except ReturnHRESULT as err:
(hresult, text) = err.args
return ReportError(text, iid=interface._iid_, clsid=clsid, hresult=hresult)
except (COMError, WindowsError) as details:
_error(
"Exception in %s.%s implementation:",
interface.__name__,
mthname,
exc_info=True,
)
return HRESULT_FROM_WIN32(winerror(details))
except E_NotImplemented:
_warning("Unimplemented method %s.%s called", interface.__name__, mthname)
return E_NOTIMPL
except:
_error(
"Exception in %s.%s implementation:",
interface.__name__,
mthname,
exc_info=True,
)
return ReportException(E_FAIL, interface._iid_, clsid=clsid)
if result is None:
return S_OK
return resultIt will log the error when an exception happens. |
This is a fix for the NULL pointer access error introduced by #17592 and reported in this comment. According to Microsoft's documentation, the pcbWritten parameter in ISequentialStream::Write and the plibNewPosition parameter in IStream::Seek can be NULL, in which case the function should ignore the output parameter and succeed. Description of user facing changes None Description of development approach ISequentialStream_RemoteWrite and IStream_RemoteSeek are changed to use the low level implementation. This makes checking the output parameter easier. Then, check if the output pointer is NULL before assigning the output value.
Link to issue number:
Closes #13284
Summary of the issue:
Currently, SAPI5 and MSSP voices use their own audio output mechanisms, instead of using the WavePlayer (WASAPI) inside NVDA.
This may make them less responsive compared to eSpeak and OneCore voices, which are using the WavePlayer, or compared to other screen readers using SAPI5 voices, according to my test result.
This also gives NVDA less control of audio output. For example, audio ducking logic inside WavePlayer cannot be applied to SAPI5 voices, so additional code is required to compensate for this.
Description of user facing changes
SAPI5 and MSSP voices will be changed to use the WavePlayer, which may make them more responsive (have less delay).
According to my test result, this can reduce the delay by at least 50ms.
This haven't trimmed the leading silence yet. If we do that also, we can expect the delay to be even less.
Description of development approach
Instead of setting
self.tts.audioOutputto a real output device, do the following:SynthDriverAudioStreamto implement COM interfaceIStream, which can be used to stream in audio data from the voices.SpCustomStreamobject to wrapSynthDriverAudioStreamand provide the wave format.SpCustomStreamobject toself.tts.AudioOutputStream, so SAPI will output audio to this stream instead.Each time an audio chunk needs to be streamed in,
ISequentialStream_RemoteWritewill be called, and we just feed the audio to the player.IStream_RemoteSeekcan also be called when SAPI wants to know the current byte position of the stream (dlibMoveshould be zero anddwOriginshould beSTREAM_SEEK_CURin this case), but it is not used to actually "seek" to a new position.IStream_Commitcan be called by MSSP voices to "flush" the audio data, where we do nothing. Other methods are left unimplemented, as they are not used when acting as an audio output stream.Previously,
comtypes.client.GetEventswas used to get the event notifications. But those notifications will be routed to the main thread via the main message loop. According to the documentation ofISpNotifySource:Because the audio data is generated and sent via
IStreamon a dedicated thread, receiving events on the main thread can make synchronizing events and audio difficult.So here
SapiSinkis changed to become an implementation ofISpNotifySink. Notifications received viaISpNotifySinkare "free-threaded", sent on the original thread instead of being routed to the main thread.ISpNotifySource::SetNotifySink.ISpEventSource::GetEvents. Events can contain pointers to objects or memory, so they need to be freed manually.Finally, all audio ducking related code are removed. Now WavePlayer should be able to handle audio ducking when using SAPI5 and MSSP voices.
There should be no change to public APIs.
Testing strategy:
Tested the delay of some built-in SAPI5 voices.
Audio ducking seemed to be working.
Stability of this is not proven yet, which needs further tests.
Known issues with pull request:
None yet
Code Review Checklist:
@coderabbitai summary