rbutil: completely rework how tts and encoders are configured. (FS#10070)
[kugel-rb.git] / rbutil / rbutilqt / tts.cpp
blobd55ba9e739a2ba167a2f076275d31365057f4657
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
9 * Copyright (C) 2007 by Dominik Wenger
10 * $Id$
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.
15 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
16 * KIND, either express or implied.
18 ****************************************************************************/
20 #include "tts.h"
21 #include "utils.h"
22 /*********************************************************************
23 * TTS Base
24 **********************************************************************/
25 QMap<QString,QString> TTSBase::ttsList;
27 TTSBase::TTSBase(QObject* parent): EncTtsSettingInterface(parent)
32 // static functions
33 void TTSBase::initTTSList()
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
46 // function to get a specific encoder
47 TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName)
50 TTSBase* tts;
51 #if defined(Q_OS_WIN)
52 if(ttsName == "sapi")
54 tts = new TTSSapi(parent);
55 return tts;
57 else
58 #endif
59 #if defined(Q_OS_LINUX)
60 if (ttsName == "festival")
62 tts = new TTSFestival(parent);
63 return tts;
65 else
66 #endif
67 if (true) // fix for OS other than WIN or LINUX
69 tts = new TTSExes(ttsName,parent);
70 return tts;
74 // get the list of encoders, nice names
75 QStringList TTSBase::getTTSList()
77 // init list if its empty
78 if(ttsList.count() == 0)
79 initTTSList();
81 return ttsList.keys();
84 // get nice name of a specific tts
85 QString TTSBase::getTTSName(QString tts)
87 if(ttsList.isEmpty())
88 initTTSList();
89 return ttsList.value(tts);
93 /*********************************************************************
94 * General TTS Exes
95 **********************************************************************/
96 TTSExes::TTSExes(QString name,QObject* parent) : TTSBase(parent)
98 m_name = name;
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\"";
106 void TTSExes::generateSettings()
108 QString exepath =settings->subValue(m_name,RbSettings::TtsPath).toString();
109 if(exepath == "") exepath = findExecutable(m_name);
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)));
115 void TTSExes::saveSettings()
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();
122 bool TTSExes::start(QString *errStr)
124 m_TTSexec = settings->subValue(m_name,RbSettings::TtsPath).toString();
125 m_TTSOpts = settings->subValue(m_name,RbSettings::TtsOptions).toString();
127 m_TTSTemplate = m_TemplateMap.value(m_name);
129 QFileInfo tts(m_TTSexec);
130 if(tts.exists())
132 return true;
134 else
136 *errStr = tr("TTS executable not found");
137 return false;
141 TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr)
143 (void) errStr;
144 QString execstring = m_TTSTemplate;
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;
156 bool TTSExes::configOk()
158 QString path = settings->subValue(m_name,RbSettings::TtsPath).toString();
160 if (QFileInfo(path).exists())
161 return true;
163 return false;
166 /*********************************************************************
167 * TTS Sapi
168 **********************************************************************/
169 TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent)
171 m_TTSTemplate = "cscript //nologo \"%exe\" /language:%lang /voice:\"%voice\" /speed:%speed \"%options\"";
172 defaultLanguage ="english";
173 m_sapi4 =false;
176 void TTSSapi::generateSettings()
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)));
195 void TTSSapi::saveSettings()
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());
203 settings->sync();
206 void TTSSapi::updateVoiceList()
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("");
215 bool TTSSapi::start(QString *errStr)
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();
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";
228 QFileInfo tts(m_TTSexec);
229 if(!tts.exists())
231 *errStr = tr("Could not copy the Sapi-script");
232 return false;
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);
242 if(m_sapi4)
243 execstring.append(" /sapi4 ");
245 qDebug() << "init" << execstring;
246 voicescript = new QProcess(NULL);
247 //connect(voicescript,SIGNAL(readyReadStandardError()),this,SLOT(error()));
249 voicescript->start(execstring);
250 if(!voicescript->waitForStarted())
252 *errStr = tr("Could not start the Sapi-script");
253 return false;
256 if(!voicescript->waitForReadyRead(300))
258 *errStr = voicescript->readAllStandardError();
259 if(*errStr != "")
260 return false;
263 voicestream = new QTextStream(voicescript);
264 voicestream->setCodec("UTF16-LE");
266 return true;
270 QStringList TTSSapi::getVoiceList(QString language)
272 QStringList result;
274 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
275 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
277 QFileInfo tts(m_TTSexec);
278 if(!tts.exists())
279 return result;
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);
286 if(settings->value(RbSettings::TtsUseSapi4).toBool())
287 execstring.append(" /sapi4 ");
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();
298 QString dataRaw = voicescript->readAllStandardError().data();
299 result = dataRaw.split(",",QString::SkipEmptyParts);
300 if(result.size() > 0)
302 result.sort();
303 result.removeFirst();
304 for(int i = 0; i< result.size();i++)
306 result[i] = result.at(i).simplified();
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;
321 TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr)
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;
333 bool TTSSapi::stop()
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;
349 bool TTSSapi::configOk()
351 if(settings->subValue("sapi",RbSettings::TtsVoice).toString().isEmpty())
352 return false;
353 return true;
355 /**********************************************************************
356 * TSSFestival - client-server wrapper
357 **********************************************************************/
358 TTSFestival::~TTSFestival()
360 stop();
363 void TTSFestival::generateSettings()
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));
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));
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);
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);
387 void TTSFestival::saveSettings()
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());
394 settings->sync();
397 void TTSFestival::updateVoiceDescription()
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);
404 void TTSFestival::clearVoiceDescription()
406 getSetting(eVOICEDESC)->setCurrent("");
409 void TTSFestival::updateVoiceList()
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("");
417 void TTSFestival::startServer(QString path)
419 if(!configOk())
420 return;
422 if(path == "")
423 path = settings->subValue("festival-server",RbSettings::TtsPath).toString();
425 serverProcess.start(QString("%1 --server").arg(path));
426 serverProcess.waitForStarted();
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";
435 void TTSFestival::ensureServerRunning(QString path)
437 if(serverProcess.state() != QProcess::Running)
439 startServer(path);
443 bool TTSFestival::start(QString* errStr)
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()));
451 return true;
454 bool TTSFestival::stop()
456 serverProcess.terminate();
457 serverProcess.kill();
459 return true;
462 TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
464 qDebug() << text << "->" << wavfile;
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;
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"))
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 */
486 clientProcess.closeReadChannel(QProcess::StandardError);
487 clientProcess.closeReadChannel(QProcess::StandardOutput);
488 clientProcess.terminate();
489 clientProcess.kill();
491 return NoError;
494 bool TTSFestival::configOk()
496 QString serverPath = settings->subValue("festival-server",RbSettings::TtsPath).toString();
497 QString clientPath = settings->subValue("festival-client",RbSettings::TtsVoice).toString();
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;
506 QStringList TTSFestival::getVoiceList(QString path)
508 if(!configOk())
509 return QStringList();
511 if(voices.size() > 0)
513 qDebug() << "Using voice cache";
514 return voices;
517 QString response = queryServer("(voice.list)",3000,path);
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();
523 voices = response.mid(1, response.size()-2).split(' ');
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.";
533 return voices;
536 QString TTSFestival::getVoiceInfo(QString voice,QString path)
538 if(!configOk())
539 return "";
541 if(!getVoiceList().contains(voice))
542 return "";
544 if(voiceDescriptions.contains(voice))
545 return voiceDescriptions[voice];
547 QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000,path);
549 if (response == "")
551 voiceDescriptions[voice]=tr("No description available");
553 else
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
561 QString description;
562 foreach(QString line, responseLines)
564 line = line.remove('(');
565 line = line.simplified();
567 line[0] = line[0].toUpper(); // capitalize the key
569 int firstSpace = line.indexOf(' ');
570 if (firstSpace > 0)
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
576 description += line + "\n";
578 voiceDescriptions[voice] = description.trimmed();
581 return voiceDescriptions[voice];
584 QString TTSFestival::queryServer(QString query, int timeout,QString path)
586 if(!configOk())
587 return "";
589 // this operation could take some time
590 emit busy();
592 ensureServerRunning(path);
594 qDebug() << "queryServer with " << query;
595 QString response;
597 QDateTime endTime;
598 if(timeout > 0)
599 endTime = QDateTime::currentDateTime().addMSecs(timeout);
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.
605 while(true)
607 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
608 QTcpSocket socket;
610 socket.connectToHost("localhost", 1314);
611 socket.waitForConnected();
613 if(socket.state() == QAbstractSocket::ConnectedState)
615 socket.write(QString("%1\n").arg(query).toAscii());
616 socket.waitForBytesWritten();
617 socket.waitForReadyRead();
619 response = socket.readAll().trimmed();
621 if (response != "LP" && response != "")
622 break;
624 socket.abort();
625 socket.disconnectFromHost();
627 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
629 emit busyEnd();
630 return "";
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);
637 if(response == "nil")
639 emit busyEnd();
640 return "";
643 QStringList lines = response.split('\n');
644 if(lines.size() > 2)
646 lines.removeFirst();
647 lines.removeLast();
649 else
650 qDebug() << "Response too short: " << response;
652 emit busyEnd();
653 return lines.join("\n");