[PATCH 0/1] MR10986: sapi: Implement ISpVoice::GetStatus.
Previously this method returned E_NOTIMPL. Populate SPVOICESTATUS from speech_voice state: - ulCurrentStream tracks the stream currently being processed by speak_proc (set at the start of the worker so it reflects the in-flight stream, and sticks at the last processed value when idle, per MSDN). Stored as last_stream_num on speech_voice. - ulLastStreamQueued reuses the existing cur_stream_num counter, which is incremented before each Speak queues a task. - hrLastResult is the HRESULT of the most recently completed speak (set at the end of speak_proc). - dwRunningState is derived from async_wait_queue_empty(&queue, 0) so it correctly reports SPRS_DONE both when idle and after a purge clears queued tasks. This unblocks Overwatch 2's Audio Captions accessibility TTS, which tight-polls GetStatus for SPRS_DONE between Speak calls; the previous E_NOTIMPL stub left the status struct untouched, leaving the game wedged forever waiting for SPRS_DONE. Add tests covering GetStatus with a NULL pointer and status fields before, during and after sync and async Speak calls. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59749 -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10986
From: Bohdan Tkachenko <bohdan@tkachenko.dev> Previously this method returned E_NOTIMPL. Populate SPVOICESTATUS from speech_voice state: - ulCurrentStream tracks the stream currently being processed by speak_proc (set at the start of the worker so it reflects the in-flight stream, and sticks at the last processed value when idle, per MSDN). Stored as last_stream_num on speech_voice. - ulLastStreamQueued reuses the existing cur_stream_num counter, which is incremented before each Speak queues a task. - hrLastResult is the HRESULT of the most recently completed speak (set at the end of speak_proc). - dwRunningState is derived from async_wait_queue_empty(&queue, 0) so it correctly reports SPRS_DONE both when idle and after a purge clears queued tasks. This unblocks Overwatch 2's Audio Captions accessibility TTS, which tight-polls GetStatus for SPRS_DONE between Speak calls; the previous E_NOTIMPL stub left the status struct untouched, leaving the game wedged forever waiting for SPRS_DONE. Add tests covering GetStatus with a NULL pointer and status fields before, during and after sync and async Speak calls. Wine-Bug: https://bugs.winehq.org/show_bug.cgi?id=59749 --- dlls/sapi/tests/tts.c | 40 ++++++++++++++++++++++++++++++++++++++++ dlls/sapi/tts.c | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/dlls/sapi/tests/tts.c b/dlls/sapi/tests/tts.c index c89bffae5a0..429a2541528 100644 --- a/dlls/sapi/tests/tts.c +++ b/dlls/sapi/tests/tts.c @@ -494,6 +494,7 @@ static void test_spvoice(void) ISpDataKey *attrs_key; LONG rate; USHORT volume; + SPVOICESTATUS status; ULONG stream_num; DWORD regid; WAVEFORMATEX wfx; @@ -651,6 +652,17 @@ static void test_spvoice(void) hr = ISpVoice_SetVolume(voice, 101); ok(hr == E_INVALIDARG, "got %#lx.\n", hr); + hr = ISpVoice_GetStatus(voice, NULL, NULL); + ok(hr == S_OK, "got %#lx.\n", hr); + + memset(&status, 0xcc, sizeof(status)); + hr = ISpVoice_GetStatus(voice, &status, NULL); + ok(hr == S_OK, "got %#lx.\n", hr); + ok(status.dwRunningState == SPRS_DONE, "got %#lx.\n", status.dwRunningState); + ok(status.ulLastStreamQueued == 0, "got %lu.\n", status.ulLastStreamQueued); + ok(status.ulCurrentStream == 0, "got %lu.\n", status.ulCurrentStream); + ok(status.hrLastResult == S_OK, "got %#lx.\n", status.hrLastResult); + hr = CoRegisterClassObject(&CLSID_TestEngine, (IUnknown *)&test_engine_cf, CLSCTX_INPROC_SERVER, REGCLS_MULTIPLEUSE, ®id); ok(hr == S_OK, "got %#lx.\n", hr); @@ -732,6 +744,16 @@ static void test_spvoice(void) ok(hr == S_OK, "got %#lx.\n", hr); ok(duration < 200, "took %lu ms.\n", duration); + memset(&status, 0xcc, sizeof(status)); + hr = ISpVoice_GetStatus(voice, &status, NULL); + ok(hr == S_OK, "got %#lx.\n", hr); + ok(status.dwRunningState == SPRS_DONE, "got %#lx.\n", status.dwRunningState); + ok(status.ulLastStreamQueued == stream_num, "got %lu vs %lu.\n", + status.ulLastStreamQueued, stream_num); + ok(status.ulCurrentStream == stream_num, "got %lu vs %lu.\n", + status.ulCurrentStream, stream_num); + ok(status.hrLastResult == S_OK, "got %#lx.\n", status.hrLastResult); + reset_engine_params(&test_engine); test_engine.output_data = wave_data; test_engine.output_len = wave_len; @@ -744,6 +766,16 @@ static void test_spvoice(void) todo_wine ok(stream_num == 1, "got %lu.\n", stream_num); ok(duration < 500, "took %lu ms.\n", duration); + memset(&status, 0xcc, sizeof(status)); + hr = ISpVoice_GetStatus(voice, &status, NULL); + ok(hr == S_OK, "got %#lx.\n", hr); + /* dwRunningState may be 0 transiently on Windows before the worker + * picks up the queued task. */ + ok(status.dwRunningState == SPRS_IS_SPEAKING || status.dwRunningState == 0, + "got %#lx.\n", status.dwRunningState); + ok(status.ulLastStreamQueued == stream_num, "got %lu vs %lu.\n", + status.ulLastStreamQueued, stream_num); + hr = ISpVoice_WaitUntilDone(voice, 100); ok(hr == S_FALSE, "got %#lx.\n", hr); @@ -752,6 +784,14 @@ static void test_spvoice(void) ok(hr == S_OK, "got %#lx.\n", hr); ok(duration > 800 && duration < 3500, "took %lu ms.\n", duration); + memset(&status, 0xcc, sizeof(status)); + hr = ISpVoice_GetStatus(voice, &status, NULL); + ok(hr == S_OK, "got %#lx.\n", hr); + ok(status.dwRunningState == SPRS_DONE, "got %#lx.\n", status.dwRunningState); + ok(status.ulCurrentStream == stream_num, "got %lu vs %lu.\n", + status.ulCurrentStream, stream_num); + ok(status.hrLastResult == S_OK, "got %#lx.\n", status.hrLastResult); + ok(test_engine.speak_called, "ISpTTSEngine::Speak was not called.\n"); ok(test_engine.flags == SPF_NLP_SPEAK_PUNC, "got %#lx.\n", test_engine.flags); ok(test_engine.frag_count == 1, "got %Iu.\n", test_engine.frag_count); diff --git a/dlls/sapi/tts.c b/dlls/sapi/tts.c index 159635a4559..bf6bf0e0d0b 100644 --- a/dlls/sapi/tts.c +++ b/dlls/sapi/tts.c @@ -58,6 +58,8 @@ struct speech_voice LONG rate; SPVSTATE state; struct async_queue queue; + ULONG last_stream_num; + HRESULT last_hr; CRITICAL_SECTION cs; }; @@ -959,6 +961,11 @@ static void speak_proc(struct async_task *task) EnterCriticalSection(&This->cs); + /* Update ulCurrentStream tracking now, before doing any work — per MSDN + * SPVOICESTATUS.ulCurrentStream reports the stream currently being + * processed, and sticks at the last processed value once idle. */ + This->last_stream_num = site->stream_num; + if (This->actions & SPVES_ABORT) { LeaveCriticalSection(&This->cs); @@ -997,6 +1004,11 @@ done: ISpAudio_Release(audio); } CoTaskMemFree(wfx); + + EnterCriticalSection(&This->cs); + This->last_hr = hr; + LeaveCriticalSection(&This->cs); + ISpTTSEngine_Release(speak_task->engine); free_frag_list(speak_task->frag_list); ISpTTSEngineSite_Release(speak_task->site); @@ -1211,14 +1223,26 @@ static HRESULT WINAPI spvoice_SpeakStream(ISpVoice *iface, IStream *stream, DWOR static HRESULT WINAPI spvoice_GetStatus(ISpVoice *iface, SPVOICESTATUS *status, WCHAR **bookmark) { - static unsigned int once; + struct speech_voice *This = impl_from_ISpVoice(iface); - if (!once++) - FIXME("(%p, %p, %p): stub.\n", iface, status, bookmark); - else - WARN("(%p, %p, %p): stub.\n", iface, status, bookmark); + TRACE("(%p, %p, %p).\n", iface, status, bookmark); - return E_NOTIMPL; + if (bookmark) *bookmark = NULL; + + /* Windows accepts a NULL status pointer as a no-op returning S_OK. */ + if (!status) return S_OK; + + memset(status, 0, sizeof(*status)); + + EnterCriticalSection(&This->cs); + status->ulCurrentStream = This->last_stream_num; + status->ulLastStreamQueued = This->cur_stream_num; + status->hrLastResult = This->last_hr; + status->dwRunningState = (async_wait_queue_empty(&This->queue, 0) == WAIT_OBJECT_0) ? + SPRS_DONE : SPRS_IS_SPEAKING; + LeaveCriticalSection(&This->cs); + + return S_OK; } static HRESULT WINAPI spvoice_Skip(ISpVoice *iface, const WCHAR *type, LONG items, ULONG *skipped) @@ -1714,6 +1738,8 @@ HRESULT speech_voice_create(IUnknown *outer, REFIID iid, void **obj) This->actions = SPVES_CONTINUE; This->volume = 100; This->rate = 0; + This->last_stream_num = 0; + This->last_hr = S_OK; memset(&This->state, 0, sizeof(This->state)); This->state.Volume = 100; -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10986
This merge request was closed by Bohdan Tkachenko. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10986
participants (2)
-
Bohdan Tkachenko -
Bohdan Tkachenko (@BohdanTkachenko)