Start work on event handling with windows SAPI

This commit is contained in:
Kovid Goyal 2020-11-19 16:49:18 +05:30
parent 21bd53d398
commit 3309f3728f
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
2 changed files with 104 additions and 2 deletions

View File

@ -3,15 +3,28 @@
# License: GPL v3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from threading import Thread
class Client:
def __init__(self):
from calibre.utils.windows.winsapi import ISpVoice
self.sp_voice = ISpVoice()
self.events_thread = Thread(name='SAPIEvents', target=self.wait_for_events, daemon=True)
self.events_thread.start()
def __del__(self):
self.sp_voice.shutdown_event_loop()
self.events_thread.join(5)
self.sp_voice = None
def wait_for_events(self):
self.sp_voice.run_event_loop(self.process_event)
def process_event(self, stream_number, event_type, event_data=None):
pass
def speak_simple_text(self, text):
from calibre_extensions.winsapi import SPF_ASYNC, SPF_PURGEBEFORESPEAK, SPF_IS_NOT_XML
self.sp_voice.speak(text, SPF_ASYNC | SPF_PURGEBEFORESPEAK | SPF_IS_NOT_XML)

View File

@ -22,6 +22,7 @@ extern CComModule _Module;
typedef struct {
PyObject_HEAD
ISpVoice *voice;
HANDLE shutdown_events_thread, events_available;
} Voice;
@ -44,6 +45,28 @@ Voice_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
Py_CLEAR(self);
return error_from_hresult(hr, "Failed to create ISpVoice instance");
}
if (FAILED(hr = self->voice->SetNotifyWin32Event())) {
Py_CLEAR(self);
return error_from_hresult(hr, "Failed to set event based notify mechanism");
}
self->events_available = self->voice->GetNotifyEventHandle();
if (self->events_available == INVALID_HANDLE_VALUE) {
Py_CLEAR(self);
PyErr_SetString(PyExc_OSError, "Failed to get events handle for ISpVoice");
return NULL;
}
self->shutdown_events_thread = CreateEvent(NULL, true, false, NULL);
if (self->shutdown_events_thread == INVALID_HANDLE_VALUE) {
Py_CLEAR(self);
PyErr_SetFromWindowsErr(0);
return NULL;
}
ULONGLONG events = SPFEI(SPEI_START_INPUT_STREAM) | SPFEI(SPEI_END_INPUT_STREAM) | SPFEI(SPEI_TTS_BOOKMARK);
if (FAILED(hr = self->voice->SetInterest(events, events))) {
CloseHandle(self->shutdown_events_thread);
Py_CLEAR(self);
return error_from_hresult(hr, "Failed to register event interest");
}
}
return (PyObject*)self;
@ -52,6 +75,10 @@ Voice_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
static void
Voice_dealloc(Voice *self) {
if (self->voice) { self->voice->Release(); self->voice = NULL; }
if (self->shutdown_events_thread != INVALID_HANDLE_VALUE) {
CloseHandle(self->shutdown_events_thread);
self->shutdown_events_thread = INVALID_HANDLE_VALUE;
}
CoUninitialize();
}
// }}}
@ -246,7 +273,7 @@ Voice_speak(Voice *self, PyObject *args) {
hr = self->voice->Speak(text_or_path.ptr(), flags, &stream_number);
Py_END_ALLOW_THREADS;
if (FAILED(hr)) return error_from_hresult(hr, "Failed to speak", PyTuple_GET_ITEM(args, 0));
return PyLong_FromLong(stream_number);
return PyLong_FromUnsignedLong(stream_number);
}
static PyObject*
@ -305,6 +332,66 @@ Voice_create_recording_wav(Voice *self, PyObject *args) {
Py_RETURN_NONE;
}
static PyObject*
Voice_shutdown_event_loop(Voice *self, PyObject *args) {
if (!SetEvent(self->shutdown_events_thread)) return PyErr_SetFromWindowsErr(0);
Py_RETURN_NONE;
}
static inline void
dispatch_events(Voice *self, PyObject *callback) {
HRESULT hr;
const ULONG asz = 32;
ULONG num_events;
SPEVENT events[asz];
PyObject *ret;
long long val;
int etype;
while (true) {
Py_BEGIN_ALLOW_THREADS;
hr = self->voice->GetEvents(asz, events, &num_events);
Py_END_ALLOW_THREADS;
if (hr != S_OK && hr != S_FALSE) break;
if (num_events == 0) break;
for (ULONG i = 0; i < num_events; i++) {
etype = events[i].eEventId;
#define CALL(fmt, ...) { ret = PyObject_CallFunction(callback, fmt, __VA_ARGS__); if (ret) Py_DECREF(ret); else PyErr_Print(); } break;
switch(etype) {
case SPEI_TTS_BOOKMARK:
val = events[i].wParam;
CALL("kiL", events[i].ulStreamNum, etype, val);
case SPEI_START_INPUT_STREAM:
case SPEI_END_INPUT_STREAM:
CALL("ki", events[i].ulStreamNum, etype);
}
#undef CALL
}
}
}
static PyObject*
Voice_run_event_loop(Voice *self, PyObject *callback) {
if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback object is not callable"); return NULL; }
HANDLE handles[2] = {self->shutdown_events_thread, self->events_available};
bool keep_going = true;
DWORD ev;
while(keep_going) {
Py_BEGIN_ALLOW_THREADS;
ev = WaitForMultipleObjects(2, handles, true, INFINITE);
Py_END_ALLOW_THREADS;
switch (ev) {
case WAIT_OBJECT_0:
keep_going = false;
break;
case WAIT_OBJECT_0 + 1:
dispatch_events(self, callback);
break;
}
}
Py_RETURN_NONE;
}
// Boilerplate {{{
#define M(name, args) { #name, (PyCFunction)Voice_##name, args, ""}
static PyMethodDef Voice_methods[] = {
@ -326,6 +413,8 @@ static PyMethodDef Voice_methods[] = {
M(set_current_volume, METH_VARARGS),
M(set_current_sound_output, METH_VARARGS),
M(shutdown_event_loop, METH_NOARGS),
M(run_event_loop, METH_O),
{NULL, NULL, 0, NULL}
};
#undef M
@ -501,7 +590,7 @@ exec_module(PyObject *m) {
AI(SPEI_RESERVED1);
AI(SPEI_RESERVED2);
#undef AI
return 0;
}
static PyModuleDef_Slot slots[] = { {Py_mod_exec, (void*)exec_module}, {0, NULL} };