summaryrefslogtreecommitdiff
path: root/rbutil/rbutilqt/base/tts.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'rbutil/rbutilqt/base/tts.cpp')
-rw-r--r--rbutil/rbutilqt/base/tts.cpp656
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**********************************************************************/
25QMap<QString,QString> TTSBase::ttsList;
26
27TTSBase::TTSBase(QObject* parent): EncTtsSettingInterface(parent)
28{
29
30}
31
32// static functions
33void 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
47TTSBase* 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
75QStringList 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
85QString 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**********************************************************************/
96TTSExes::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
106void 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
115void 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
122bool 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
141TTSStatus 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
156bool 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**********************************************************************/
169TTSSapi::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
176void 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
195void 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
206void 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
215bool 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
270QStringList 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
321TTSStatus 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
333bool 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
349bool 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 **********************************************************************/
358TTSFestival::~TTSFestival()
359{
360 stop();
361}
362
363void 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
387void 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
397void 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
404void TTSFestival::clearVoiceDescription()
405{
406 getSetting(eVOICEDESC)->setCurrent("");
407}
408
409void 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
417void 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
435void TTSFestival::ensureServerRunning(QString path)
436{
437 if(serverProcess.state() != QProcess::Running)
438 {
439 startServer(path);
440 }
441}
442
443bool 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
454bool TTSFestival::stop()
455{
456 serverProcess.terminate();
457 serverProcess.kill();
458
459 return true;
460}
461
462TTSStatus 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
494bool 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
506QStringList 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
536QString 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
584QString 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