Add a SAPI synthetizer for Windows
[vlc.git] / modules / text_renderer / sapi.cpp
1 /*****************************************************************************
2  * sapi.cpp: Simple text to Speech renderer for Windows, based on SAPI
3  *****************************************************************************
4  * Copyright (c) 2015 Moti Zilberman
5  *
6  * Authors: Moti Zilberman
7  *          Jean-Baptiste Kempf
8  *
9  * The MIT License (MIT)
10  *
11  * Permission is hereby granted, free of charge, to any person obtaining a copy
12  * of this software and associated documentation files (the "Software"), to deal
13  * in the Software without restriction, including without limitation the rights
14  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15  * copies of the Software, and to permit persons to whom the Software is
16  * furnished to do so, subject to the following conditions:
17  *
18  * The above copyright notice and this permission notice shall be included in all
19  * copies or substantial portions of the Software.
20  *
21  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27  * SOFTWARE.
28  *****************************************************************************/
29
30 #ifdef HAVE_CONFIG_H
31 # include "config.h"
32 #endif
33
34 /* VLC core API headers */
35 #include <vlc_common.h>
36 #include <vlc_plugin.h>
37 #include <vlc_filter.h>
38 #include <vlc_charset.h>
39
40 #define INITGUID
41
42 #include <windows.h>
43 #include <sapi.h>
44 #include <sphelper.h>
45
46 static int Create (vlc_object_t *);
47 static void Destroy(vlc_object_t *);
48 static int RenderText(filter_t *,
49                       subpicture_region_t *,
50                       subpicture_region_t *,
51                       const vlc_fourcc_t *);
52
53 vlc_module_begin ()
54  set_description(N_("Speech synthesis for Windows"))
55
56  set_category(CAT_VIDEO)
57  set_subcategory(SUBCAT_VIDEO_SUBPIC)
58
59  set_capability("text renderer", 0)
60  set_callbacks(Create, Destroy)
61  add_integer("sapi-voice", -1, "Voice Index", "Voice index", false)
62 vlc_module_end ()
63
64 struct filter_sys_t
65 {
66     ISpVoice* cpVoice;
67     char* lastString;
68 };
69
70 /* MTA functions */
71 static int TryEnterMTA(vlc_object_t *obj)
72 {
73     HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
74     if (unlikely(FAILED(hr)))
75     {
76         msg_Err (obj, "cannot initialize COM (error 0x%lx)", hr);
77         return -1;
78     }
79     return 0;
80 }
81 #define TryEnterMTA(o) TryEnterMTA(VLC_OBJECT(o))
82
83 static void EnterMTA(void)
84 {
85     HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
86     if (unlikely(FAILED(hr)))
87         abort();
88 }
89
90 static void LeaveMTA(void)
91 {
92     CoUninitialize();
93 }
94
95 static int Create (vlc_object_t *p_this)
96 {
97     filter_t *p_filter = (filter_t *)p_this;
98     filter_sys_t *p_sys;
99     HRESULT hr;
100
101     if (TryEnterMTA(p_this))
102         return VLC_EGENERIC;
103
104     p_filter->p_sys = p_sys = (filter_sys_t*) malloc(sizeof(filter_sys_t));
105     if (!p_sys)
106         goto error;
107
108     p_sys->cpVoice = nullptr;
109     p_sys->lastString = nullptr;
110
111     hr = CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_INPROC_SERVER, IID_ISpVoice, (void**) &p_sys->cpVoice);
112     if (SUCCEEDED(hr)) {
113         ISpObjectToken*        cpVoiceToken = nullptr;
114         IEnumSpObjectTokens*   cpEnum = nullptr;
115         ULONG ulCount = 0;
116
117         hr = SpEnumTokens(SPCAT_VOICES, nullptr, nullptr, &cpEnum);
118         if (SUCCEEDED(hr))
119         {
120             // Get the number of voices.
121             hr = cpEnum->GetCount(&ulCount);
122             if (SUCCEEDED (hr))
123             {
124                 int voiceIndex = var_InheritInteger(p_this, "sapi-voice");
125                 if (voiceIndex > -1)
126                 {
127                     if ((unsigned)voiceIndex <= ulCount) {
128                         hr = cpEnum->Item(voiceIndex, &cpVoiceToken);
129                         if (SUCCEEDED(hr)) {
130                             hr = p_sys->cpVoice->SetVoice(cpVoiceToken);
131                             if (SUCCEEDED(hr)) {
132                                 msg_Dbg(p_this, "Selected voice %d", voiceIndex);
133                             }
134                             else {
135                                 msg_Err(p_this, "Failed to set voice %d", voiceIndex);
136                             }
137                             cpVoiceToken->Release();
138                             cpVoiceToken = nullptr;
139                         }
140                     }
141                     else
142                         msg_Err(p_this, "Voice index exceeds available count");
143                 }
144             }
145             cpEnum->Release();
146             cpEnum = nullptr;
147
148             /* Set Output */
149             hr = p_sys->cpVoice->SetOutput(nullptr, TRUE);
150         }
151     }
152     else
153     {
154         msg_Err(p_filter, "Could not create SpVoice");
155         goto error;
156     }
157
158     LeaveMTA();
159
160     p_filter->pf_render = RenderText;
161
162     return VLC_SUCCESS;
163
164 error:
165     LeaveMTA();
166     free(p_sys);
167     return VLC_EGENERIC;
168 }
169
170 static void Destroy(vlc_object_t *p_this)
171 {
172     filter_t *p_filter = (filter_t *)p_this;
173     filter_sys_t *p_sys = p_filter->p_sys;
174
175     if (p_sys->cpVoice) {
176         p_sys->cpVoice->Release();
177         p_sys->cpVoice = nullptr;
178     }
179
180     if (p_sys->lastString) {
181         free(p_sys->lastString);
182         p_sys->lastString = nullptr;
183     }
184
185     free(p_sys);
186 }
187
188 static int RenderText(filter_t *p_filter,
189         subpicture_region_t *p_region_out,
190         subpicture_region_t *p_region_in,
191         const vlc_fourcc_t *p_chroma_list)
192 {
193     VLC_UNUSED(p_region_out);
194     VLC_UNUSED(p_chroma_list);
195
196     filter_sys_t *p_sys = p_filter->p_sys;
197     text_segment_t *p_segment = p_region_in->p_text;
198
199     if (!p_segment)
200         return VLC_EGENERIC;
201
202     for (const text_segment_t *s = p_segment; s != nullptr; s = s->p_next ) {
203         if (!s->psz_text )
204             continue;
205
206         if (strlen(s->psz_text) == 0)
207             continue;
208
209         try {
210             if (p_sys->lastString && !strcmp(p_sys->lastString, s->psz_text))
211                 continue;
212
213             if (!strcmp(s->psz_text, "\n"))
214                 continue;
215
216             p_sys->lastString = strdup(s->psz_text);
217             if (p_sys->lastString) {
218                 msg_Dbg(p_filter, "Speaking '%s'", s->psz_text);
219
220                 EnterMTA();
221                 wchar_t* wideText = ToWide(s->psz_text);
222                 HRESULT hr = p_sys->cpVoice->Speak(wideText, SPF_ASYNC, nullptr);
223                 free(wideText);
224                 if (!SUCCEEDED(hr)) {
225                     msg_Err(p_filter, "Speak() error");
226                 }
227                 LeaveMTA();
228             }
229         }
230         catch (...) {
231             msg_Err(p_filter, "Caught an exception!");
232         }
233     }
234
235     return VLC_SUCCESS;
236 }