[PATCH 0/1] MR10906: sapi: Implement ISpVoice::GetStatus, SetPriority and GetPriority.
Previously these three methods returned E_NOTIMPL. SetPriority/GetPriority store the priority on the voice and validate it against SPVPRI_NORMAL, SPVPRI_ALERT and SPVPRI_OVER. GetStatus populates 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 calls SetPriority during voice setup and tight-polls GetStatus for SPRS_DONE between Speak calls. Add tests covering priority round-trips, 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/10906
From: Bohdan Tkachenko <bohdan@tkachenko.dev> Previously these three methods returned E_NOTIMPL. SetPriority/GetPriority store the priority on the voice and validate it against SPVPRI_NORMAL, SPVPRI_ALERT and SPVPRI_OVER. GetStatus populates 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 calls SetPriority during voice setup and tight-polls GetStatus for SPRS_DONE between Speak calls. Add tests covering priority round-trips, 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 | 62 +++++++++++++++++++++++++++++++++++++++++ dlls/sapi/tts.c | 64 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/dlls/sapi/tests/tts.c b/dlls/sapi/tests/tts.c index c89bffae5a0..6e5f781618b 100644 --- a/dlls/sapi/tests/tts.c +++ b/dlls/sapi/tests/tts.c @@ -494,6 +494,8 @@ static void test_spvoice(void) ISpDataKey *attrs_key; LONG rate; USHORT volume; + SPVPRIORITY priority; + SPVOICESTATUS status; ULONG stream_num; DWORD regid; WAVEFORMATEX wfx; @@ -651,6 +653,41 @@ static void test_spvoice(void) hr = ISpVoice_SetVolume(voice, 101); ok(hr == E_INVALIDARG, "got %#lx.\n", hr); + priority = 0xdead; + hr = ISpVoice_GetPriority(voice, &priority); + ok(hr == S_OK, "got %#lx.\n", hr); + ok(priority == SPVPRI_NORMAL, "got %d.\n", priority); + + hr = ISpVoice_SetPriority(voice, SPVPRI_ALERT); + ok(hr == S_OK, "got %#lx.\n", hr); + + priority = 0xdead; + hr = ISpVoice_GetPriority(voice, &priority); + ok(hr == S_OK, "got %#lx.\n", hr); + ok(priority == SPVPRI_ALERT, "got %d.\n", priority); + + hr = ISpVoice_SetPriority(voice, SPVPRI_OVER); + ok(hr == S_OK, "got %#lx.\n", hr); + + priority = 0xdead; + hr = ISpVoice_GetPriority(voice, &priority); + ok(hr == S_OK, "got %#lx.\n", hr); + ok(priority == SPVPRI_OVER, "got %d.\n", priority); + + hr = ISpVoice_SetPriority(voice, SPVPRI_NORMAL); + ok(hr == S_OK, "got %#lx.\n", hr); + + hr = ISpVoice_GetStatus(voice, NULL, NULL); + ok(hr == E_POINTER, "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 +769,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 +791,13 @@ 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); + ok(status.dwRunningState == SPRS_IS_SPEAKING, "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 +806,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..626b5fdbaa0 100644 --- a/dlls/sapi/tts.c +++ b/dlls/sapi/tts.c @@ -58,6 +58,9 @@ struct speech_voice LONG rate; SPVSTATE state; struct async_queue queue; + SPVPRIORITY priority; + ULONG last_stream_num; + HRESULT last_hr; CRITICAL_SECTION cs; }; @@ -959,6 +962,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 +1005,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 +1224,25 @@ 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 (!status) return E_POINTER; + + 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); + + if (bookmark) *bookmark = NULL; + + return S_OK; } static HRESULT WINAPI spvoice_Skip(ISpVoice *iface, const WCHAR *type, LONG items, ULONG *skipped) @@ -1230,16 +1254,33 @@ static HRESULT WINAPI spvoice_Skip(ISpVoice *iface, const WCHAR *type, LONG item static HRESULT WINAPI spvoice_SetPriority(ISpVoice *iface, SPVPRIORITY priority) { - FIXME("(%p, %d): stub.\n", iface, priority); + struct speech_voice *This = impl_from_ISpVoice(iface); - return E_NOTIMPL; + TRACE("(%p, %d).\n", iface, priority); + + if (priority != SPVPRI_NORMAL && priority != SPVPRI_ALERT && priority != SPVPRI_OVER) + return E_INVALIDARG; + + EnterCriticalSection(&This->cs); + This->priority = priority; + LeaveCriticalSection(&This->cs); + + return S_OK; } static HRESULT WINAPI spvoice_GetPriority(ISpVoice *iface, SPVPRIORITY *priority) { - FIXME("(%p, %p): stub.\n", iface, priority); + struct speech_voice *This = impl_from_ISpVoice(iface); - return E_NOTIMPL; + TRACE("(%p, %p).\n", iface, priority); + + if (!priority) return E_POINTER; + + EnterCriticalSection(&This->cs); + *priority = This->priority; + LeaveCriticalSection(&This->cs); + + return S_OK; } static HRESULT WINAPI spvoice_SetAlertBoundary(ISpVoice *iface, SPEVENTENUM boundary) @@ -1714,6 +1755,9 @@ HRESULT speech_voice_create(IUnknown *outer, REFIID iid, void **obj) This->actions = SPVES_CONTINUE; This->volume = 100; This->rate = 0; + This->priority = SPVPRI_NORMAL; + 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/10906
participants (2)
-
Bohdan Tkachenko -
Bohdan Tkachenko (@BohdanTkachenko)