From 61d44ea95ef69fa79da4c60f857bd5d8d010d0f7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 22 Oct 2020 13:35:09 +0530 Subject: [PATCH] Implement getting and setting current voice --- src/calibre/utils/windows/common.h | 7 ++-- src/calibre/utils/windows/winsapi.cpp | 41 ++++++++++++++++++++--- src/calibre/utils/windows/winsapi.py | 48 +++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 7 deletions(-) create mode 100644 src/calibre/utils/windows/winsapi.py diff --git a/src/calibre/utils/windows/common.h b/src/calibre/utils/windows/common.h index deaa70fbf5..654a8901d8 100644 --- a/src/calibre/utils/windows/common.h +++ b/src/calibre/utils/windows/common.h @@ -13,11 +13,13 @@ #include static inline PyObject* -set_error_from_hresult(const char *file, const int line, const HRESULT hr, const char *prefix="") { +set_error_from_hresult(const char *file, const int line, const HRESULT hr, const char *prefix="", PyObject *name=NULL) { _com_error err(hr); LPCWSTR msg = err.ErrorMessage(); PyObject *pmsg = PyUnicode_FromWideChar(msg, -1); - PyObject *ans = PyErr_Format(PyExc_OSError, "%s:%d:%s:%V", file, line, prefix, pmsg, "Out of memory"); + PyObject *ans; + if (name) ans = PyErr_Format(PyExc_OSError, "%s:%d:%s:%V: %S", file, line, prefix, pmsg, "Out of memory", name); + else ans = PyErr_Format(PyExc_OSError, "%s:%d:%s:%V", file, line, prefix, pmsg, "Out of memory"); Py_CLEAR(pmsg); return ans; } @@ -41,6 +43,7 @@ class wchar_raii { wchar_t *ptr() { return handle; } void set_ptr(wchar_t *val) { handle = val; } + explicit operator bool() const { return handle != NULL; } }; diff --git a/src/calibre/utils/windows/winsapi.cpp b/src/calibre/utils/windows/winsapi.cpp index cb03160472..60d8df4337 100644 --- a/src/calibre/utils/windows/winsapi.cpp +++ b/src/calibre/utils/windows/winsapi.cpp @@ -20,7 +20,7 @@ extern CComModule _Module; typedef struct { PyObject_HEAD - ISpVoice *voice; + ISpVoice *voice; } Voice; @@ -81,7 +81,36 @@ Voice_get_all_sound_outputs(Voice *self, PyObject *args) { if (PyList_Append(ans.ptr(), dict.ptr()) != 0) return NULL; } - return ans.detach(); + return PyList_AsTuple(ans.ptr()); +} + +static PyObject* +Voice_get_current_voice(Voice *self, PyObject *args) { + HRESULT hr = S_OK; + CComPtr token = NULL; + if (FAILED(hr = self->voice->GetVoice(&token))) { + return error_from_hresult(hr, "Failed to get current voice"); + } + com_wchar_raii id; + if (FAILED(hr = token->GetId(id.address()))) return error_from_hresult(hr, "Failed to get ID for current voice"); + return PyUnicode_FromWideChar(id.ptr(), -1); +} + +static PyObject* +Voice_set_current_voice(Voice *self, PyObject *args) { + wchar_raii id; + if (!PyArg_ParseTuple(args, "|O&", py_to_wchar, &id)) return NULL; + HRESULT hr = S_OK; + if (id) { + CComPtr token = NULL; + if (FAILED(hr = SpGetTokenFromId(id.ptr(), &token))) { + return error_from_hresult(hr, "Failed to find voice with id", PyTuple_GET_ITEM(args, 0)); + } + if (FAILED(hr = self->voice->SetVoice(token))) return error_from_hresult(hr, "Failed to set voice to default"); + } else { + if (FAILED(hr = self->voice->SetVoice(NULL))) return error_from_hresult(hr, "Failed to set voice to default"); + } + Py_RETURN_NONE; } static PyObject* @@ -131,13 +160,15 @@ Voice_get_all_voices(Voice *self, PyObject *args) { } if (PyList_Append(ans.ptr(), dict.ptr()) != 0) return NULL; } - return ans.detach(); + return PyList_AsTuple(ans.ptr()); } #define M(name, args) { #name, (PyCFunction)Voice_##name, args, ""} static PyMethodDef Voice_methods[] = { M(get_all_voices, METH_NOARGS), + M(get_current_voice, METH_NOARGS), + M(set_current_voice, METH_VARARGS), M(get_all_sound_outputs, METH_NOARGS), {NULL, NULL, 0, NULL} }; @@ -166,7 +197,7 @@ static struct PyModuleDef winsapi_module = { extern "C" { CALIBRE_MODINIT_FUNC PyInit_winsapi(void) { - VoiceType.tp_name = "winsapi.Voice"; + VoiceType.tp_name = "winsapi.ISpVoice"; VoiceType.tp_doc = "Wrapper for ISpVoice"; VoiceType.tp_basicsize = sizeof(Voice); VoiceType.tp_itemsize = 0; @@ -180,7 +211,7 @@ CALIBRE_MODINIT_FUNC PyInit_winsapi(void) { if (m == NULL) return NULL; Py_INCREF(&VoiceType); - if (PyModule_AddObject(m, "Voice", (PyObject *) &VoiceType) < 0) { + if (PyModule_AddObject(m, "ISpVoice", (PyObject *) &VoiceType) < 0) { Py_DECREF(&VoiceType); Py_DECREF(m); return NULL; diff --git a/src/calibre/utils/windows/winsapi.py b/src/calibre/utils/windows/winsapi.py new file mode 100644 index 0000000000..906a7df154 --- /dev/null +++ b/src/calibre/utils/windows/winsapi.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +# License: GPL v3 Copyright: 2020, Kovid Goyal + + +from calibre_extensions.winsapi import ISpVoice + + +def develop(): + from pprint import pprint + spv = ISpVoice() + voices = spv.get_all_voices() + pprint(voices) + for voice in voices: + spv.set_current_voice(voice['id']) + + +def find_tests(): + import unittest + + class TestSAPI(unittest.TestCase): + + def setUp(self): + self.sapi = ISpVoice() + + def tearDown(self): + self.sapi = None + + def test_enumeration_of_voices(self): + default_voice = self.sapi.get_current_voice() + self.assertTrue(default_voice) + all_voices = self.sapi.get_all_voices() + self.assertTrue(all_voices) + self.assertIn(default_voice, {x['id'] for x in all_voices}) + for voice in all_voices: + for key in ('name', 'gender', 'age', 'language'): + self.assertIn(key, voice) + self.sapi.set_current_voice(voice['id']) + self.assertEqual(self.sapi.get_current_voice(), voice['id']) + self.sapi.set_current_voice() + self.assertEqual(self.sapi.get_current_voice(), default_voice) + + return unittest.defaultTestLoader.loadTestsFromTestCase(TestSAPI) + + +def run_tests(): + from calibre.utils.run_tests import run_tests + run_tests(find_tests)