diff options
Diffstat (limited to 'rbutil/rbutilqt/base/tts.cpp')
-rw-r--r-- | rbutil/rbutilqt/base/tts.cpp | 656 |
1 files changed, 656 insertions, 0 deletions
diff --git a/rbutil/rbutilqt/base/tts.cpp b/rbutil/rbutilqt/base/tts.cpp new file mode 100644 index 0000000000..d55ba9e739 --- /dev/null +++ b/rbutil/rbutilqt/base/tts.cpp | |||
@@ -0,0 +1,656 @@ | |||
1 | /*************************************************************************** | ||
2 | * __________ __ ___. | ||
3 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | * \/ \/ \/ \/ \/ | ||
8 | * | ||
9 | * Copyright (C) 2007 by Dominik Wenger | ||
10 | * $Id$ | ||
11 | * | ||
12 | * All files in this archive are subject to the GNU General Public License. | ||
13 | * See the file COPYING in the source tree root for full license agreement. | ||
14 | * | ||
15 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
16 | * KIND, either express or implied. | ||
17 | * | ||
18 | ****************************************************************************/ | ||
19 | |||
20 | #include "tts.h" | ||
21 | #include "utils.h" | ||
22 | /********************************************************************* | ||
23 | * TTS Base | ||
24 | **********************************************************************/ | ||
25 | QMap<QString,QString> TTSBase::ttsList; | ||
26 | |||
27 | TTSBase::TTSBase(QObject* parent): EncTtsSettingInterface(parent) | ||
28 | { | ||
29 | |||
30 | } | ||
31 | |||
32 | // static functions | ||
33 | void TTSBase::initTTSList() | ||
34 | { | ||
35 | ttsList["espeak"] = "Espeak TTS Engine"; | ||
36 | ttsList["flite"] = "Flite TTS Engine"; | ||
37 | ttsList["swift"] = "Swift TTS Engine"; | ||
38 | #if defined(Q_OS_WIN) | ||
39 | ttsList["sapi"] = "Sapi TTS Engine"; | ||
40 | #endif | ||
41 | #if defined(Q_OS_LINUX) | ||
42 | ttsList["festival"] = "Festival TTS Engine"; | ||
43 | #endif | ||
44 | } | ||
45 | |||
46 | // function to get a specific encoder | ||
47 | TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName) | ||
48 | { | ||
49 | |||
50 | TTSBase* tts; | ||
51 | #if defined(Q_OS_WIN) | ||
52 | if(ttsName == "sapi") | ||
53 | { | ||
54 | tts = new TTSSapi(parent); | ||
55 | return tts; | ||
56 | } | ||
57 | else | ||
58 | #endif | ||
59 | #if defined(Q_OS_LINUX) | ||
60 | if (ttsName == "festival") | ||
61 | { | ||
62 | tts = new TTSFestival(parent); | ||
63 | return tts; | ||
64 | } | ||
65 | else | ||
66 | #endif | ||
67 | if (true) // fix for OS other than WIN or LINUX | ||
68 | { | ||
69 | tts = new TTSExes(ttsName,parent); | ||
70 | return tts; | ||
71 | } | ||
72 | } | ||
73 | |||
74 | // get the list of encoders, nice names | ||
75 | QStringList TTSBase::getTTSList() | ||
76 | { | ||
77 | // init list if its empty | ||
78 | if(ttsList.count() == 0) | ||
79 | initTTSList(); | ||
80 | |||
81 | return ttsList.keys(); | ||
82 | } | ||
83 | |||
84 | // get nice name of a specific tts | ||
85 | QString TTSBase::getTTSName(QString tts) | ||
86 | { | ||
87 | if(ttsList.isEmpty()) | ||
88 | initTTSList(); | ||
89 | return ttsList.value(tts); | ||
90 | } | ||
91 | |||
92 | |||
93 | /********************************************************************* | ||
94 | * General TTS Exes | ||
95 | **********************************************************************/ | ||
96 | TTSExes::TTSExes(QString name,QObject* parent) : TTSBase(parent) | ||
97 | { | ||
98 | m_name = name; | ||
99 | |||
100 | m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\""; | ||
101 | m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" -t \"%text\""; | ||
102 | m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\""; | ||
103 | |||
104 | } | ||
105 | |||
106 | void TTSExes::generateSettings() | ||
107 | { | ||
108 | QString exepath =settings->subValue(m_name,RbSettings::TtsPath).toString(); | ||
109 | if(exepath == "") exepath = findExecutable(m_name); | ||
110 | |||
111 | insertSetting(eEXEPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to TTS engine:",exepath,EncTtsSetting::eBROWSEBTN)); | ||
112 | insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,"TTS enginge options:",settings->subValue(m_name,RbSettings::TtsOptions))); | ||
113 | } | ||
114 | |||
115 | void TTSExes::saveSettings() | ||
116 | { | ||
117 | settings->setSubValue(m_name,RbSettings::TtsPath,getSetting(eEXEPATH)->current().toString()); | ||
118 | settings->setSubValue(m_name,RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString()); | ||
119 | settings->sync(); | ||
120 | } | ||
121 | |||
122 | bool TTSExes::start(QString *errStr) | ||
123 | { | ||
124 | m_TTSexec = settings->subValue(m_name,RbSettings::TtsPath).toString(); | ||
125 | m_TTSOpts = settings->subValue(m_name,RbSettings::TtsOptions).toString(); | ||
126 | |||
127 | m_TTSTemplate = m_TemplateMap.value(m_name); | ||
128 | |||
129 | QFileInfo tts(m_TTSexec); | ||
130 | if(tts.exists()) | ||
131 | { | ||
132 | return true; | ||
133 | } | ||
134 | else | ||
135 | { | ||
136 | *errStr = tr("TTS executable not found"); | ||
137 | return false; | ||
138 | } | ||
139 | } | ||
140 | |||
141 | TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr) | ||
142 | { | ||
143 | (void) errStr; | ||
144 | QString execstring = m_TTSTemplate; | ||
145 | |||
146 | execstring.replace("%exe",m_TTSexec); | ||
147 | execstring.replace("%options",m_TTSOpts); | ||
148 | execstring.replace("%wavfile",wavfile); | ||
149 | execstring.replace("%text",text); | ||
150 | //qDebug() << "voicing" << execstring; | ||
151 | QProcess::execute(execstring); | ||
152 | return NoError; | ||
153 | |||
154 | } | ||
155 | |||
156 | bool TTSExes::configOk() | ||
157 | { | ||
158 | QString path = settings->subValue(m_name,RbSettings::TtsPath).toString(); | ||
159 | |||
160 | if (QFileInfo(path).exists()) | ||
161 | return true; | ||
162 | |||
163 | return false; | ||
164 | } | ||
165 | |||
166 | /********************************************************************* | ||
167 | * TTS Sapi | ||
168 | **********************************************************************/ | ||
169 | TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent) | ||
170 | { | ||
171 | m_TTSTemplate = "cscript //nologo \"%exe\" /language:%lang /voice:\"%voice\" /speed:%speed \"%options\""; | ||
172 | defaultLanguage ="english"; | ||
173 | m_sapi4 =false; | ||
174 | } | ||
175 | |||
176 | void TTSSapi::generateSettings() | ||
177 | { | ||
178 | // language | ||
179 | QStringList languages = settings->languages(); | ||
180 | languages.sort(); | ||
181 | EncTtsSetting* setting =new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,"Language:",settings->subValue("sapi",RbSettings::TtsLanguage),languages); | ||
182 | connect(setting,SIGNAL(dataChanged()),this,SLOT(updateVoiceList())); | ||
183 | insertSetting(eLANGUAGE,setting); | ||
184 | // voice | ||
185 | setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,"Voice:",settings->subValue("sapi",RbSettings::TtsVoice),getVoiceList(settings->subValue("sapi",RbSettings::TtsLanguage).toString()),EncTtsSetting::eREFRESHBTN); | ||
186 | connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList())); | ||
187 | insertSetting(eVOICE,setting); | ||
188 | //speed | ||
189 | insertSetting(eSPEED,new EncTtsSetting(this,EncTtsSetting::eINT,"Speed:",settings->subValue("sapi",RbSettings::TtsSpeed),-10,10)); | ||
190 | // options | ||
191 | insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Options:",settings->subValue("sapi",RbSettings::TtsOptions))); | ||
192 | |||
193 | } | ||
194 | |||
195 | void TTSSapi::saveSettings() | ||
196 | { | ||
197 | //save settings in user config | ||
198 | settings->setSubValue("sapi",RbSettings::TtsLanguage,getSetting(eLANGUAGE)->current().toString()); | ||
199 | settings->setSubValue("sapi",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); | ||
200 | settings->setSubValue("sapi",RbSettings::TtsSpeed,getSetting(eSPEED)->current().toInt()); | ||
201 | settings->setSubValue("sapi",RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString()); | ||
202 | |||
203 | settings->sync(); | ||
204 | } | ||
205 | |||
206 | void TTSSapi::updateVoiceList() | ||
207 | { | ||
208 | qDebug() << "update voiceList"; | ||
209 | QStringList voiceList = getVoiceList(getSetting(eLANGUAGE)->current().toString()); | ||
210 | getSetting(eVOICE)->setList(voiceList); | ||
211 | if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); | ||
212 | else getSetting(eVOICE)->setCurrent(""); | ||
213 | } | ||
214 | |||
215 | bool TTSSapi::start(QString *errStr) | ||
216 | { | ||
217 | |||
218 | m_TTSOpts = settings->subValue("sapi",RbSettings::TtsOptions).toString(); | ||
219 | m_TTSLanguage =settings->subValue("sapi",RbSettings::TtsLanguage).toString(); | ||
220 | m_TTSVoice=settings->subValue("sapi",RbSettings::TtsVoice).toString(); | ||
221 | m_TTSSpeed=settings->subValue("sapi",RbSettings::TtsSpeed).toString(); | ||
222 | m_sapi4 = settings->subValue("sapi",RbSettings::TtsUseSapi4).toBool(); | ||
223 | |||
224 | QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); | ||
225 | QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs"); | ||
226 | m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs"; | ||
227 | |||
228 | QFileInfo tts(m_TTSexec); | ||
229 | if(!tts.exists()) | ||
230 | { | ||
231 | *errStr = tr("Could not copy the Sapi-script"); | ||
232 | return false; | ||
233 | } | ||
234 | // create the voice process | ||
235 | QString execstring = m_TTSTemplate; | ||
236 | execstring.replace("%exe",m_TTSexec); | ||
237 | execstring.replace("%options",m_TTSOpts); | ||
238 | execstring.replace("%lang",m_TTSLanguage); | ||
239 | execstring.replace("%voice",m_TTSVoice); | ||
240 | execstring.replace("%speed",m_TTSSpeed); | ||
241 | |||
242 | if(m_sapi4) | ||
243 | execstring.append(" /sapi4 "); | ||
244 | |||
245 | qDebug() << "init" << execstring; | ||
246 | voicescript = new QProcess(NULL); | ||
247 | //connect(voicescript,SIGNAL(readyReadStandardError()),this,SLOT(error())); | ||
248 | |||
249 | voicescript->start(execstring); | ||
250 | if(!voicescript->waitForStarted()) | ||
251 | { | ||
252 | *errStr = tr("Could not start the Sapi-script"); | ||
253 | return false; | ||
254 | } | ||
255 | |||
256 | if(!voicescript->waitForReadyRead(300)) | ||
257 | { | ||
258 | *errStr = voicescript->readAllStandardError(); | ||
259 | if(*errStr != "") | ||
260 | return false; | ||
261 | } | ||
262 | |||
263 | voicestream = new QTextStream(voicescript); | ||
264 | voicestream->setCodec("UTF16-LE"); | ||
265 | |||
266 | return true; | ||
267 | } | ||
268 | |||
269 | |||
270 | QStringList TTSSapi::getVoiceList(QString language) | ||
271 | { | ||
272 | QStringList result; | ||
273 | |||
274 | QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs"); | ||
275 | m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs"; | ||
276 | |||
277 | QFileInfo tts(m_TTSexec); | ||
278 | if(!tts.exists()) | ||
279 | return result; | ||
280 | |||
281 | // create the voice process | ||
282 | QString execstring = "cscript //nologo \"%exe\" /language:%lang /listvoices"; | ||
283 | execstring.replace("%exe",m_TTSexec); | ||
284 | execstring.replace("%lang",language); | ||
285 | |||
286 | if(settings->value(RbSettings::TtsUseSapi4).toBool()) | ||
287 | execstring.append(" /sapi4 "); | ||
288 | |||
289 | qDebug() << "init" << execstring; | ||
290 | voicescript = new QProcess(NULL); | ||
291 | voicescript->start(execstring); | ||
292 | qDebug() << "wait for started"; | ||
293 | if(!voicescript->waitForStarted()) | ||
294 | return result; | ||
295 | voicescript->closeWriteChannel(); | ||
296 | voicescript->waitForReadyRead(); | ||
297 | |||
298 | QString dataRaw = voicescript->readAllStandardError().data(); | ||
299 | result = dataRaw.split(",",QString::SkipEmptyParts); | ||
300 | if(result.size() > 0) | ||
301 | { | ||
302 | result.sort(); | ||
303 | result.removeFirst(); | ||
304 | for(int i = 0; i< result.size();i++) | ||
305 | { | ||
306 | result[i] = result.at(i).simplified(); | ||
307 | } | ||
308 | } | ||
309 | |||
310 | delete voicescript; | ||
311 | QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner | ||
312 | |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser | ||
313 | |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup | ||
314 | |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther ); | ||
315 | QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); | ||
316 | return result; | ||
317 | } | ||
318 | |||
319 | |||
320 | |||
321 | TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr) | ||
322 | { | ||
323 | (void) errStr; | ||
324 | QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n"; | ||
325 | qDebug() << "voicing" << query; | ||
326 | *voicestream << query; | ||
327 | *voicestream << "SYNC\tbla\r\n"; | ||
328 | voicestream->flush(); | ||
329 | voicescript->waitForReadyRead(); | ||
330 | return NoError; | ||
331 | } | ||
332 | |||
333 | bool TTSSapi::stop() | ||
334 | { | ||
335 | |||
336 | *voicestream << "QUIT\r\n"; | ||
337 | voicestream->flush(); | ||
338 | voicescript->waitForFinished(); | ||
339 | delete voicestream; | ||
340 | delete voicescript; | ||
341 | QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner | ||
342 | |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser | ||
343 | |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup | ||
344 | |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther ); | ||
345 | QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); | ||
346 | return true; | ||
347 | } | ||
348 | |||
349 | bool TTSSapi::configOk() | ||
350 | { | ||
351 | if(settings->subValue("sapi",RbSettings::TtsVoice).toString().isEmpty()) | ||
352 | return false; | ||
353 | return true; | ||
354 | } | ||
355 | /********************************************************************** | ||
356 | * TSSFestival - client-server wrapper | ||
357 | **********************************************************************/ | ||
358 | TTSFestival::~TTSFestival() | ||
359 | { | ||
360 | stop(); | ||
361 | } | ||
362 | |||
363 | void TTSFestival::generateSettings() | ||
364 | { | ||
365 | // server path | ||
366 | QString exepath = settings->subValue("festival-server",RbSettings::TtsPath).toString(); | ||
367 | if(exepath == "" ) exepath = findExecutable("festival"); | ||
368 | insertSetting(eSERVERPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival server:",exepath,EncTtsSetting::eBROWSEBTN)); | ||
369 | |||
370 | // client path | ||
371 | QString clientpath = settings->subValue("festival-client",RbSettings::TtsPath).toString(); | ||
372 | if(clientpath == "" ) clientpath = findExecutable("festival_client"); | ||
373 | insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival client:",clientpath,EncTtsSetting::eBROWSEBTN)); | ||
374 | |||
375 | // voice | ||
376 | EncTtsSetting* setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,"Voice:",settings->subValue("festival",RbSettings::TtsVoice),getVoiceList(exepath),EncTtsSetting::eREFRESHBTN); | ||
377 | connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList())); | ||
378 | connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription())); | ||
379 | insertSetting(eVOICE,setting); | ||
380 | |||
381 | //voice description | ||
382 | setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,"Voice description:","",EncTtsSetting::eREFRESHBTN); | ||
383 | connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription())); | ||
384 | insertSetting(eVOICEDESC,setting); | ||
385 | } | ||
386 | |||
387 | void TTSFestival::saveSettings() | ||
388 | { | ||
389 | //save settings in user config | ||
390 | settings->setSubValue("festival-server",RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString()); | ||
391 | settings->setSubValue("festival-client",RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString()); | ||
392 | settings->setSubValue("festival",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); | ||
393 | |||
394 | settings->sync(); | ||
395 | } | ||
396 | |||
397 | void TTSFestival::updateVoiceDescription() | ||
398 | { | ||
399 | // get voice Info with current voice and path | ||
400 | QString info = getVoiceInfo(getSetting(eVOICE)->current().toString(),getSetting(eSERVERPATH)->current().toString()); | ||
401 | getSetting(eVOICEDESC)->setCurrent(info); | ||
402 | } | ||
403 | |||
404 | void TTSFestival::clearVoiceDescription() | ||
405 | { | ||
406 | getSetting(eVOICEDESC)->setCurrent(""); | ||
407 | } | ||
408 | |||
409 | void TTSFestival::updateVoiceList() | ||
410 | { | ||
411 | QStringList voiceList = getVoiceList(getSetting(eSERVERPATH)->current().toString()); | ||
412 | getSetting(eVOICE)->setList(voiceList); | ||
413 | if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); | ||
414 | else getSetting(eVOICE)->setCurrent(""); | ||
415 | } | ||
416 | |||
417 | void TTSFestival::startServer(QString path) | ||
418 | { | ||
419 | if(!configOk()) | ||
420 | return; | ||
421 | |||
422 | if(path == "") | ||
423 | path = settings->subValue("festival-server",RbSettings::TtsPath).toString(); | ||
424 | |||
425 | serverProcess.start(QString("%1 --server").arg(path)); | ||
426 | serverProcess.waitForStarted(); | ||
427 | |||
428 | queryServer("(getpid)",300,path); | ||
429 | if(serverProcess.state() == QProcess::Running) | ||
430 | qDebug() << "Festival is up and running"; | ||
431 | else | ||
432 | qDebug() << "Festival failed to start"; | ||
433 | } | ||
434 | |||
435 | void TTSFestival::ensureServerRunning(QString path) | ||
436 | { | ||
437 | if(serverProcess.state() != QProcess::Running) | ||
438 | { | ||
439 | startServer(path); | ||
440 | } | ||
441 | } | ||
442 | |||
443 | bool TTSFestival::start(QString* errStr) | ||
444 | { | ||
445 | (void) errStr; | ||
446 | ensureServerRunning(); | ||
447 | if (!settings->subValue("festival",RbSettings::TtsVoice).toString().isEmpty()) | ||
448 | queryServer(QString("(voice.select '%1)") | ||
449 | .arg(settings->subValue("festival", RbSettings::TtsVoice).toString())); | ||
450 | |||
451 | return true; | ||
452 | } | ||
453 | |||
454 | bool TTSFestival::stop() | ||
455 | { | ||
456 | serverProcess.terminate(); | ||
457 | serverProcess.kill(); | ||
458 | |||
459 | return true; | ||
460 | } | ||
461 | |||
462 | TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr) | ||
463 | { | ||
464 | qDebug() << text << "->" << wavfile; | ||
465 | |||
466 | QString path = settings->subValue("festival-client",RbSettings::TtsPath).toString(); | ||
467 | QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(path).arg(wavfile); | ||
468 | qDebug() << cmd; | ||
469 | |||
470 | QProcess clientProcess; | ||
471 | clientProcess.start(cmd); | ||
472 | clientProcess.write(QString("%1.\n").arg(text).toAscii()); | ||
473 | clientProcess.waitForBytesWritten(); | ||
474 | clientProcess.closeWriteChannel(); | ||
475 | clientProcess.waitForReadyRead(); | ||
476 | QString response = clientProcess.readAll(); | ||
477 | response = response.trimmed(); | ||
478 | if(!response.contains("Utterance")) | ||
479 | { | ||
480 | qDebug() << "Could not voice string: " << response; | ||
481 | *errStr = tr("engine could not voice string"); | ||
482 | return Warning; | ||
483 | /* do not stop the voicing process because of a single string | ||
484 | TODO: needs proper settings */ | ||
485 | } | ||
486 | clientProcess.closeReadChannel(QProcess::StandardError); | ||
487 | clientProcess.closeReadChannel(QProcess::StandardOutput); | ||
488 | clientProcess.terminate(); | ||
489 | clientProcess.kill(); | ||
490 | |||
491 | return NoError; | ||
492 | } | ||
493 | |||
494 | bool TTSFestival::configOk() | ||
495 | { | ||
496 | QString serverPath = settings->subValue("festival-server",RbSettings::TtsPath).toString(); | ||
497 | QString clientPath = settings->subValue("festival-client",RbSettings::TtsVoice).toString(); | ||
498 | |||
499 | bool ret = QFileInfo(serverPath).isExecutable() && | ||
500 | QFileInfo(clientPath).isExecutable(); | ||
501 | if(settings->subValue("festival",RbSettings::TtsVoice).toString().size() > 0 && voices.size() > 0) | ||
502 | ret = ret && (voices.indexOf(settings->subValue("festival",RbSettings::TtsVoice).toString()) != -1); | ||
503 | return ret; | ||
504 | } | ||
505 | |||
506 | QStringList TTSFestival::getVoiceList(QString path) | ||
507 | { | ||
508 | if(!configOk()) | ||
509 | return QStringList(); | ||
510 | |||
511 | if(voices.size() > 0) | ||
512 | { | ||
513 | qDebug() << "Using voice cache"; | ||
514 | return voices; | ||
515 | } | ||
516 | |||
517 | QString response = queryServer("(voice.list)",3000,path); | ||
518 | |||
519 | // get the 2nd line. It should be (<voice_name>, <voice_name>) | ||
520 | response = response.mid(response.indexOf('\n') + 1, -1); | ||
521 | response = response.left(response.indexOf('\n')).trimmed(); | ||
522 | |||
523 | voices = response.mid(1, response.size()-2).split(' '); | ||
524 | |||
525 | voices.sort(); | ||
526 | if (voices.size() == 1 && voices[0].size() == 0) | ||
527 | voices.removeAt(0); | ||
528 | if (voices.size() > 0) | ||
529 | qDebug() << "Voices: " << voices; | ||
530 | else | ||
531 | qDebug() << "No voices."; | ||
532 | |||
533 | return voices; | ||
534 | } | ||
535 | |||
536 | QString TTSFestival::getVoiceInfo(QString voice,QString path) | ||
537 | { | ||
538 | if(!configOk()) | ||
539 | return ""; | ||
540 | |||
541 | if(!getVoiceList().contains(voice)) | ||
542 | return ""; | ||
543 | |||
544 | if(voiceDescriptions.contains(voice)) | ||
545 | return voiceDescriptions[voice]; | ||
546 | |||
547 | QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000,path); | ||
548 | |||
549 | if (response == "") | ||
550 | { | ||
551 | voiceDescriptions[voice]=tr("No description available"); | ||
552 | } | ||
553 | else | ||
554 | { | ||
555 | response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard)); | ||
556 | qDebug() << "voiceInfo w/o descr: " << response; | ||
557 | response = response.remove(')'); | ||
558 | QStringList responseLines = response.split('(', QString::SkipEmptyParts); | ||
559 | responseLines.removeAt(0); // the voice name itself | ||
560 | |||
561 | QString description; | ||
562 | foreach(QString line, responseLines) | ||
563 | { | ||
564 | line = line.remove('('); | ||
565 | line = line.simplified(); | ||
566 | |||
567 | line[0] = line[0].toUpper(); // capitalize the key | ||
568 | |||
569 | int firstSpace = line.indexOf(' '); | ||
570 | if (firstSpace > 0) | ||
571 | { | ||
572 | line = line.insert(firstSpace, ':'); // add a colon between the key and the value | ||
573 | line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value | ||
574 | } | ||
575 | |||
576 | description += line + "\n"; | ||
577 | } | ||
578 | voiceDescriptions[voice] = description.trimmed(); | ||
579 | } | ||
580 | |||
581 | return voiceDescriptions[voice]; | ||
582 | } | ||
583 | |||
584 | QString TTSFestival::queryServer(QString query, int timeout,QString path) | ||
585 | { | ||
586 | if(!configOk()) | ||
587 | return ""; | ||
588 | |||
589 | // this operation could take some time | ||
590 | emit busy(); | ||
591 | |||
592 | ensureServerRunning(path); | ||
593 | |||
594 | qDebug() << "queryServer with " << query; | ||
595 | QString response; | ||
596 | |||
597 | QDateTime endTime; | ||
598 | if(timeout > 0) | ||
599 | endTime = QDateTime::currentDateTime().addMSecs(timeout); | ||
600 | |||
601 | /* Festival is *extremely* unreliable. Although at this | ||
602 | * point we are sure that SIOD is accepting commands, | ||
603 | * we might end up with an empty response. Hence, the loop. | ||
604 | */ | ||
605 | while(true) | ||
606 | { | ||
607 | QCoreApplication::processEvents(QEventLoop::AllEvents, 50); | ||
608 | QTcpSocket socket; | ||
609 | |||
610 | socket.connectToHost("localhost", 1314); | ||
611 | socket.waitForConnected(); | ||
612 | |||
613 | if(socket.state() == QAbstractSocket::ConnectedState) | ||
614 | { | ||
615 | socket.write(QString("%1\n").arg(query).toAscii()); | ||
616 | socket.waitForBytesWritten(); | ||
617 | socket.waitForReadyRead(); | ||
618 | |||
619 | response = socket.readAll().trimmed(); | ||
620 | |||
621 | if (response != "LP" && response != "") | ||
622 | break; | ||
623 | } | ||
624 | socket.abort(); | ||
625 | socket.disconnectFromHost(); | ||
626 | |||
627 | if(timeout > 0 && QDateTime::currentDateTime() >= endTime) | ||
628 | { | ||
629 | emit busyEnd(); | ||
630 | return ""; | ||
631 | } | ||
632 | /* make sure we wait a little as we don't want to flood the server with requests */ | ||
633 | QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500); | ||
634 | while(QDateTime::currentDateTime() < tmpEndTime) | ||
635 | QCoreApplication::processEvents(QEventLoop::AllEvents); | ||
636 | } | ||
637 | if(response == "nil") | ||
638 | { | ||
639 | emit busyEnd(); | ||
640 | return ""; | ||
641 | } | ||
642 | |||
643 | QStringList lines = response.split('\n'); | ||
644 | if(lines.size() > 2) | ||
645 | { | ||
646 | lines.removeFirst(); | ||
647 | lines.removeLast(); | ||
648 | } | ||
649 | else | ||
650 | qDebug() << "Response too short: " << response; | ||
651 | |||
652 | emit busyEnd(); | ||
653 | return lines.join("\n"); | ||
654 | |||
655 | } | ||
656 | |||