rbutil: add the forgotten ui file from festival support.
[kugel-rb.git] / rbutil / rbutilqt / tts.cpp
blob48555cc114f4e537e4a5851aba0f0219e8c1d389
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"
23 // static variables
24 QMap<QString,QString> TTSBase::ttsList;
25 QMap<QString,TTSBase*> TTSBase::ttsCache;
27 // static functions
28 void TTSBase::initTTSList()
30 ttsList["espeak"] = "Espeak TTS Engine";
31 ttsList["flite"] = "Flite TTS Engine";
32 ttsList["swift"] = "Swift TTS Engine";
33 #if defined(Q_OS_WIN)
34 ttsList["sapi"] = "Sapi TTS Engine";
35 #endif
36 #if defined(Q_OS_LINUX)
37 ttsList["festival"] = "Festival TTS Engine";
38 #endif
41 // function to get a specific encoder
42 TTSBase* TTSBase::getTTS(QString ttsName)
44 // check cache
45 if(ttsCache.contains(ttsName))
46 return ttsCache.value(ttsName);
48 TTSBase* tts;
49 #if defined(Q_OS_WIN)
50 if(ttsName == "sapi")
52 tts = new TTSSapi();
53 ttsCache[ttsName] = tts;
54 return tts;
56 else
57 #endif
58 #if defined(Q_OS_LINUX)
59 if (ttsName == "festival")
61 tts = new TTSFestival();
62 ttsCache[ttsName] = tts;
63 return tts;
65 else
66 #endif
67 if (true) // fix for OS other than WIN or LINUX
69 tts = new TTSExes(ttsName);
70 ttsCache[ttsName] = tts;
71 return tts;
75 // get the list of encoders, nice names
76 QStringList TTSBase::getTTSList()
78 // init list if its empty
79 if(ttsList.count() == 0)
80 initTTSList();
82 return ttsList.keys();
85 // get nice name of a specific tts
86 QString TTSBase::getTTSName(QString tts)
88 if(ttsList.isEmpty())
89 initTTSList();
90 return ttsList.value(tts);
93 /*********************************************************************
94 * TTS Base
95 **********************************************************************/
96 TTSBase::TTSBase(): QObject()
101 /*********************************************************************
102 * General TTS Exes
103 **********************************************************************/
104 TTSExes::TTSExes(QString name) : TTSBase()
106 m_name = name;
108 m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\"";
109 m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" -t \"%text\"";
110 m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\"";
114 void TTSExes::setCfg(RbSettings* sett)
116 // call function of base class
117 TTSBase::setCfg(sett);
119 // if the config isnt OK, try to autodetect
120 if(!configOk())
122 QString exepath;
123 //try autodetect tts
124 #if defined(Q_OS_LINUX) || defined(Q_OS_MACX) || defined(Q_OS_OPENBSD)
125 QStringList path = QString(getenv("PATH")).split(":", QString::SkipEmptyParts);
126 #elif defined(Q_OS_WIN)
127 QStringList path = QString(getenv("PATH")).split(";", QString::SkipEmptyParts);
128 #endif
129 qDebug() << path;
130 for(int i = 0; i < path.size(); i++)
132 QString executable = QDir::fromNativeSeparators(path.at(i)) + "/" + m_name;
133 #if defined(Q_OS_WIN)
134 executable += ".exe";
135 QStringList ex = executable.split("\"", QString::SkipEmptyParts);
136 executable = ex.join("");
137 #endif
138 qDebug() << executable;
139 if(QFileInfo(executable).isExecutable())
141 exepath= QDir::toNativeSeparators(executable);
142 break;
145 settings->setTTSPath(m_name,exepath);
146 settings->sync();
151 bool TTSExes::start(QString *errStr)
153 m_TTSexec = settings->ttsPath(m_name);
154 m_TTSOpts = settings->ttsOptions(m_name);
156 m_TTSTemplate = m_TemplateMap.value(m_name);
158 QFileInfo tts(m_TTSexec);
159 if(tts.exists())
161 return true;
163 else
165 *errStr = tr("TTS executable not found");
166 return false;
170 TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr)
172 (void) errStr;
173 QString execstring = m_TTSTemplate;
175 execstring.replace("%exe",m_TTSexec);
176 execstring.replace("%options",m_TTSOpts);
177 execstring.replace("%wavfile",wavfile);
178 execstring.replace("%text",text);
179 //qDebug() << "voicing" << execstring;
180 QProcess::execute(execstring);
181 return NoError;
185 void TTSExes::showCfg()
187 #ifndef CONSOLE
188 TTSExesGui gui;
189 #else
190 TTSExesGuiCli gui;
191 #endif
192 gui.setCfg(settings);
193 gui.showCfg(m_name);
196 bool TTSExes::configOk()
198 QString path = settings->ttsPath(m_name);
200 if (QFileInfo(path).exists())
201 return true;
203 return false;
206 /*********************************************************************
207 * TTS Sapi
208 **********************************************************************/
209 TTSSapi::TTSSapi() : TTSBase()
211 m_TTSTemplate = "cscript //nologo \"%exe\" /language:%lang /voice:\"%voice\" /speed:%speed \"%options\"";
212 defaultLanguage ="english";
213 m_sapi4 =false;
217 bool TTSSapi::start(QString *errStr)
220 m_TTSOpts = settings->ttsOptions("sapi");
221 m_TTSLanguage =settings->ttsLang("sapi");
222 m_TTSVoice=settings->ttsVoice("sapi");
223 m_TTSSpeed=QString("%1").arg(settings->ttsSpeed("sapi"));
224 m_sapi4 = settings->ttsUseSapi4();
226 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
227 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
228 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
230 QFileInfo tts(m_TTSexec);
231 if(!tts.exists())
233 *errStr = tr("Could not copy the Sapi-script");
234 return false;
236 // create the voice process
237 QString execstring = m_TTSTemplate;
238 execstring.replace("%exe",m_TTSexec);
239 execstring.replace("%options",m_TTSOpts);
240 execstring.replace("%lang",m_TTSLanguage);
241 execstring.replace("%voice",m_TTSVoice);
242 execstring.replace("%speed",m_TTSSpeed);
244 if(m_sapi4)
245 execstring.append(" /sapi4 ");
247 qDebug() << "init" << execstring;
248 voicescript = new QProcess(NULL);
249 //connect(voicescript,SIGNAL(readyReadStandardError()),this,SLOT(error()));
251 voicescript->start(execstring);
252 if(!voicescript->waitForStarted())
254 *errStr = tr("Could not start the Sapi-script");
255 return false;
258 if(!voicescript->waitForReadyRead(300))
260 *errStr = voicescript->readAllStandardError();
261 if(*errStr != "")
262 return false;
265 voicestream = new QTextStream(voicescript);
266 voicestream->setCodec("UTF16-LE");
268 return true;
272 QStringList TTSSapi::getVoiceList(QString language)
274 QStringList result;
276 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
277 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
279 QFileInfo tts(m_TTSexec);
280 if(!tts.exists())
281 return result;
283 // create the voice process
284 QString execstring = "cscript //nologo \"%exe\" /language:%lang /listvoices";
285 execstring.replace("%exe",m_TTSexec);
286 execstring.replace("%lang",language);
288 if(settings->ttsUseSapi4())
289 execstring.append(" /sapi4 ");
291 qDebug() << "init" << execstring;
292 voicescript = new QProcess(NULL);
293 voicescript->start(execstring);
294 qDebug() << "wait for started";
295 if(!voicescript->waitForStarted())
296 return result;
297 voicescript->closeWriteChannel();
298 voicescript->waitForReadyRead();
300 QString dataRaw = voicescript->readAllStandardError().data();
301 result = dataRaw.split(",",QString::SkipEmptyParts);
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");
317 return result;
322 TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr)
324 (void) errStr;
325 QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n";
326 qDebug() << "voicing" << query;
327 *voicestream << query;
328 *voicestream << "SYNC\tbla\r\n";
329 voicestream->flush();
330 voicescript->waitForReadyRead();
331 return NoError;
334 bool TTSSapi::stop()
337 *voicestream << "QUIT\r\n";
338 voicestream->flush();
339 voicescript->waitForFinished();
340 delete voicestream;
341 delete voicescript;
342 QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner
343 |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser
344 |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup
345 |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther );
346 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
347 return true;
351 void TTSSapi::showCfg()
353 #ifndef CONSOLE
354 TTSSapiGui gui(this);
355 #else
356 TTSSapiGuiCli gui(this);
357 #endif
358 gui.setCfg(settings);
359 gui.showCfg();
362 bool TTSSapi::configOk()
364 if(settings->ttsVoice("sapi").isEmpty())
365 return false;
366 return true;
368 /**********************************************************************
369 * TSSFestival - client-server wrapper
370 **********************************************************************/
371 TTSFestival::~TTSFestival()
373 stop();
376 void TTSFestival::startServer()
378 if(!configOk())
379 return;
381 QStringList paths = settings->ttsPath("festival").split(":");
383 serverProcess.start(QString("%1 --server").arg(paths[0]));
384 serverProcess.waitForStarted();
386 queryServer("(getpid)");
387 if(serverProcess.state() == QProcess::Running)
388 qDebug() << "Festival is up and running";
389 else
390 qDebug() << "Festival failed to start";
393 void TTSFestival::ensureServerRunning()
395 if(serverProcess.state() != QProcess::Running)
397 // least common denominator for all the server startup code paths
398 QProgressDialog progressDialog(tr(""), tr(""), 0, 0);
399 progressDialog.setWindowTitle(tr("Starting festival"));
400 progressDialog.setModal(true);
401 progressDialog.setLabel(0);
402 progressDialog.setCancelButton(0);
403 progressDialog.show();
405 QApplication::processEvents(); // actually show the dialog
407 startServer();
411 bool TTSFestival::start(QString* errStr)
413 (void) errStr;
414 ensureServerRunning();
415 if (!settings->ttsVoice("festival").isEmpty())
416 queryServer(QString("(voice.select '%1)").arg(settings->ttsVoice("festival")));
418 return true;
421 bool TTSFestival::stop()
423 serverProcess.terminate();
424 serverProcess.kill();
426 return true;
429 TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
431 qDebug() << text << "->" << wavfile;
433 QStringList paths = settings->ttsPath("festival").split(":");
434 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(paths[1]).arg(wavfile);
435 qDebug() << cmd;
437 QProcess clientProcess;
438 clientProcess.start(cmd);
439 clientProcess.write(QString("%1.\n").arg(text).toAscii());
440 clientProcess.waitForBytesWritten();
441 clientProcess.closeWriteChannel();
442 clientProcess.waitForReadyRead();
443 QString response = clientProcess.readAll();
444 response = response.trimmed();
445 if(!response.contains("Utterance"))
447 qDebug() << "Could not voice string: " << response;
448 *errStr = tr("engine could not voice string");
449 return Warning;
450 /* do not stop the voicing process because of a single string
451 TODO: needs proper settings */
453 clientProcess.closeReadChannel(QProcess::StandardError);
454 clientProcess.closeReadChannel(QProcess::StandardOutput);
455 clientProcess.terminate();
456 clientProcess.kill();
458 return NoError;
461 bool TTSFestival::configOk()
463 QStringList paths = settings->ttsPath("festival").split(":");
464 if(paths.size() != 2)
465 return false;
466 bool ret = QFileInfo(paths[0]).isExecutable() &&
467 QFileInfo(paths[1]).isExecutable();
468 if(settings->ttsVoice("festival").size() > 0 && voices.size() > 0)
469 ret = ret && (voices.indexOf(settings->ttsVoice("festival")) != -1);
470 return ret;
473 void TTSFestival::showCfg()
475 #ifndef CONSOLE
476 TTSFestivalGui gui(this);
477 #endif
478 gui.setCfg(settings);
479 gui.showCfg();
482 QStringList TTSFestival::getVoiceList()
484 if(!configOk())
485 return QStringList();
487 if(voices.size() > 0)
489 qDebug() << "Using voice cache";
490 return voices;
492 QString response = queryServer("(voice.list)");
494 // get the 2nd line. It should be (<voice_name>, <voice_name>)
495 response = response.mid(response.indexOf('\n') + 1, -1);
496 response = response.left(response.indexOf('\n')).trimmed();
498 voices = response.mid(1, response.size()-2).split(' ');
500 voices.sort();
501 if (voices.size() == 1 && voices[0].size() == 0)
502 voices.removeAt(0);
503 if (voices.size() > 0)
504 qDebug() << "Voices: " << voices;
505 else
506 qDebug() << "No voices.";
507 return voices;
510 QString TTSFestival::getVoiceInfo(QString voice)
512 if(!configOk())
513 return "";
515 if(!getVoiceList().contains(voice))
516 return "";
518 if(voiceDescriptions.contains(voice))
519 return voiceDescriptions[voice];
521 QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000);
523 if (response == "")
525 voiceDescriptions[voice]=tr("No description available");
527 else
529 response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard));
530 qDebug() << "voiceInfo w/o descr: " << response;
531 response = response.remove(')');
532 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
533 responseLines.removeAt(0); // the voice name itself
535 QString description;
536 foreach(QString line, responseLines)
538 line = line.remove('(');
539 line = line.simplified();
541 line[0] = line[0].toUpper(); // capitalize the key
543 int firstSpace = line.indexOf(' ');
544 if (firstSpace > 0)
546 line = line.insert(firstSpace, ':'); // add a colon between the key and the value
547 line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value
550 description += line + "\n";
552 voiceDescriptions[voice] = description.trimmed();
554 return voiceDescriptions[voice];
557 QString TTSFestival::queryServer(QString query, int timeout)
559 if(!configOk())
560 return "";
562 ensureServerRunning();
564 qDebug() << "queryServer with " << query;
565 QString response;
567 QDateTime endTime;
568 if(timeout > 0)
569 endTime = QDateTime::currentDateTime().addMSecs(timeout);
571 /* Festival is *extremely* unreliable. Although at this
572 * point we are sure that SIOD is accepting commands,
573 * we might end up with an empty response. Hence, the loop.
575 while(true)
577 QApplication::processEvents(QEventLoop::AllEvents, 50);
578 QTcpSocket socket;
580 socket.connectToHost("localhost", 1314);
581 socket.waitForConnected();
583 if(socket.state() == QAbstractSocket::ConnectedState)
585 socket.write(QString("%1\n").arg(query).toAscii());
586 socket.waitForBytesWritten();
587 socket.waitForReadyRead();
589 response = socket.readAll().trimmed();
591 if (response != "LP" && response != "")
592 break;
594 socket.abort();
595 socket.disconnectFromHost();
597 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
598 return "";
600 /* make sure we wait a little as we don't want to flood the server with requests */
601 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
602 while(QDateTime::currentDateTime() < tmpEndTime)
603 QApplication::processEvents(QEventLoop::AllEvents);
605 if(response == "nil")
606 return "";
608 QStringList lines = response.split('\n');
609 if(lines.size() > 2)
611 lines.removeFirst();
612 lines.removeLast();
614 else
615 qDebug() << "Response too short: " << response;
616 return lines.join("\n");