svn properties, late as usual
[kugel-rb.git] / rbutil / rbutilqt / base / tts.cpp
blob4b0727f78ce1fbebbc8a48fbff183e8a9e98176d
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 #include "rbsettings.h"
23 /*********************************************************************
24 * TTS Base
25 **********************************************************************/
26 QMap<QString,QString> TTSBase::ttsList;
28 TTSBase::TTSBase(QObject* parent): EncTtsSettingInterface(parent)
33 // static functions
34 void TTSBase::initTTSList()
36 ttsList["espeak"] = "Espeak TTS Engine";
37 ttsList["flite"] = "Flite TTS Engine";
38 ttsList["swift"] = "Swift TTS Engine";
39 #if defined(Q_OS_WIN)
40 ttsList["sapi"] = "Sapi TTS Engine";
41 #endif
42 #if defined(Q_OS_LINUX)
43 ttsList["festival"] = "Festival TTS Engine";
44 #endif
47 // function to get a specific encoder
48 TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName)
51 TTSBase* tts;
52 #if defined(Q_OS_WIN)
53 if(ttsName == "sapi")
55 tts = new TTSSapi(parent);
56 return tts;
58 else
59 #endif
60 #if defined(Q_OS_LINUX)
61 if (ttsName == "festival")
63 tts = new TTSFestival(parent);
64 return tts;
66 else
67 #endif
68 if (true) // fix for OS other than WIN or LINUX
70 tts = new TTSExes(ttsName,parent);
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);
94 /*********************************************************************
95 * General TTS Exes
96 **********************************************************************/
97 TTSExes::TTSExes(QString name,QObject* parent) : TTSBase(parent)
99 m_name = name;
101 m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\"";
102 m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" -t \"%text\"";
103 m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\"";
107 void TTSExes::generateSettings()
109 QString exepath =RbSettings::subValue(m_name,RbSettings::TtsPath).toString();
110 if(exepath == "") exepath = findExecutable(m_name);
112 insertSetting(eEXEPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
113 tr("Path to TTS engine:"),exepath,EncTtsSetting::eBROWSEBTN));
114 insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,
115 tr("TTS engine options:"),RbSettings::subValue(m_name,RbSettings::TtsOptions)));
118 void TTSExes::saveSettings()
120 RbSettings::setSubValue(m_name,RbSettings::TtsPath,getSetting(eEXEPATH)->current().toString());
121 RbSettings::setSubValue(m_name,RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString());
122 RbSettings::sync();
125 bool TTSExes::start(QString *errStr)
127 m_TTSexec = RbSettings::subValue(m_name,RbSettings::TtsPath).toString();
128 m_TTSOpts = RbSettings::subValue(m_name,RbSettings::TtsOptions).toString();
130 m_TTSTemplate = m_TemplateMap.value(m_name);
132 QFileInfo tts(m_TTSexec);
133 if(tts.exists())
135 return true;
137 else
139 *errStr = tr("TTS executable not found");
140 return false;
144 TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr)
146 (void) errStr;
147 QString execstring = m_TTSTemplate;
149 execstring.replace("%exe",m_TTSexec);
150 execstring.replace("%options",m_TTSOpts);
151 execstring.replace("%wavfile",wavfile);
152 execstring.replace("%text",text);
153 //qDebug() << "voicing" << execstring;
154 QProcess::execute(execstring);
155 return NoError;
159 bool TTSExes::configOk()
161 QString path = RbSettings::subValue(m_name,RbSettings::TtsPath).toString();
163 if (QFileInfo(path).exists())
164 return true;
166 return false;
169 /*********************************************************************
170 * TTS Sapi
171 **********************************************************************/
172 TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent)
174 m_TTSTemplate = "cscript //nologo \"%exe\" /language:%lang /voice:\"%voice\" /speed:%speed \"%options\"";
175 defaultLanguage ="english";
176 m_sapi4 =false;
179 void TTSSapi::generateSettings()
181 // language
182 QStringList languages = RbSettings::languages();
183 languages.sort();
184 EncTtsSetting* setting =new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,
185 tr("Language:"),RbSettings::subValue("sapi",RbSettings::TtsLanguage),languages);
186 connect(setting,SIGNAL(dataChanged()),this,SLOT(updateVoiceList()));
187 insertSetting(eLANGUAGE,setting);
188 // voice
189 setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,
190 tr("Voice:"),RbSettings::subValue("sapi",RbSettings::TtsVoice),getVoiceList(RbSettings::subValue("sapi",RbSettings::TtsLanguage).toString()),EncTtsSetting::eREFRESHBTN);
191 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
192 insertSetting(eVOICE,setting);
193 //speed
194 insertSetting(eSPEED,new EncTtsSetting(this,EncTtsSetting::eINT,
195 tr("Speed:"),RbSettings::subValue("sapi",RbSettings::TtsSpeed),-10,10));
196 // options
197 insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,
198 tr("Options:"),RbSettings::subValue("sapi",RbSettings::TtsOptions)));
202 void TTSSapi::saveSettings()
204 //save settings in user config
205 RbSettings::setSubValue("sapi",RbSettings::TtsLanguage,getSetting(eLANGUAGE)->current().toString());
206 RbSettings::setSubValue("sapi",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
207 RbSettings::setSubValue("sapi",RbSettings::TtsSpeed,getSetting(eSPEED)->current().toInt());
208 RbSettings::setSubValue("sapi",RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString());
210 RbSettings::sync();
213 void TTSSapi::updateVoiceList()
215 qDebug() << "update voiceList";
216 QStringList voiceList = getVoiceList(getSetting(eLANGUAGE)->current().toString());
217 getSetting(eVOICE)->setList(voiceList);
218 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
219 else getSetting(eVOICE)->setCurrent("");
222 bool TTSSapi::start(QString *errStr)
225 m_TTSOpts = RbSettings::subValue("sapi",RbSettings::TtsOptions).toString();
226 m_TTSLanguage =RbSettings::subValue("sapi",RbSettings::TtsLanguage).toString();
227 m_TTSVoice=RbSettings::subValue("sapi",RbSettings::TtsVoice).toString();
228 m_TTSSpeed=RbSettings::subValue("sapi",RbSettings::TtsSpeed).toString();
229 m_sapi4 = RbSettings::subValue("sapi",RbSettings::TtsUseSapi4).toBool();
231 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
232 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
233 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
235 QFileInfo tts(m_TTSexec);
236 if(!tts.exists())
238 *errStr = tr("Could not copy the Sapi-script");
239 return false;
241 // create the voice process
242 QString execstring = m_TTSTemplate;
243 execstring.replace("%exe",m_TTSexec);
244 execstring.replace("%options",m_TTSOpts);
245 execstring.replace("%lang",m_TTSLanguage);
246 execstring.replace("%voice",m_TTSVoice);
247 execstring.replace("%speed",m_TTSSpeed);
249 if(m_sapi4)
250 execstring.append(" /sapi4 ");
252 qDebug() << "init" << execstring;
253 voicescript = new QProcess(NULL);
254 //connect(voicescript,SIGNAL(readyReadStandardError()),this,SLOT(error()));
256 voicescript->start(execstring);
257 if(!voicescript->waitForStarted())
259 *errStr = tr("Could not start the Sapi-script");
260 return false;
263 if(!voicescript->waitForReadyRead(300))
265 *errStr = voicescript->readAllStandardError();
266 if(*errStr != "")
267 return false;
270 voicestream = new QTextStream(voicescript);
271 voicestream->setCodec("UTF16-LE");
273 return true;
277 QStringList TTSSapi::getVoiceList(QString language)
279 QStringList result;
281 QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs");
282 m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs";
284 QFileInfo tts(m_TTSexec);
285 if(!tts.exists())
286 return result;
288 // create the voice process
289 QString execstring = "cscript //nologo \"%exe\" /language:%lang /listvoices";
290 execstring.replace("%exe",m_TTSexec);
291 execstring.replace("%lang",language);
293 if(RbSettings::value(RbSettings::TtsUseSapi4).toBool())
294 execstring.append(" /sapi4 ");
296 qDebug() << "init" << execstring;
297 voicescript = new QProcess(NULL);
298 voicescript->start(execstring);
299 qDebug() << "wait for started";
300 if(!voicescript->waitForStarted())
301 return result;
302 voicescript->closeWriteChannel();
303 voicescript->waitForReadyRead();
305 QString dataRaw = voicescript->readAllStandardError().data();
306 result = dataRaw.split(",",QString::SkipEmptyParts);
307 if(result.size() > 0)
309 result.sort();
310 result.removeFirst();
311 for(int i = 0; i< result.size();i++)
313 result[i] = result.at(i).simplified();
317 delete voicescript;
318 QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner
319 |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser
320 |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup
321 |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther );
322 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
323 return result;
328 TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr)
330 (void) errStr;
331 QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n";
332 qDebug() << "voicing" << query;
333 *voicestream << query;
334 *voicestream << "SYNC\tbla\r\n";
335 voicestream->flush();
336 voicescript->waitForReadyRead();
337 return NoError;
340 bool TTSSapi::stop()
343 *voicestream << "QUIT\r\n";
344 voicestream->flush();
345 voicescript->waitForFinished();
346 delete voicestream;
347 delete voicescript;
348 QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner
349 |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser
350 |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup
351 |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther );
352 QFile::remove(QDir::tempPath() +"/sapi_voice.vbs");
353 return true;
356 bool TTSSapi::configOk()
358 if(RbSettings::subValue("sapi",RbSettings::TtsVoice).toString().isEmpty())
359 return false;
360 return true;
362 /**********************************************************************
363 * TSSFestival - client-server wrapper
364 **********************************************************************/
365 TTSFestival::~TTSFestival()
367 stop();
370 void TTSFestival::generateSettings()
372 // server path
373 QString exepath = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
374 if(exepath == "" ) exepath = findExecutable("festival");
375 insertSetting(eSERVERPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival server:",exepath,EncTtsSetting::eBROWSEBTN));
377 // client path
378 QString clientpath = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString();
379 if(clientpath == "" ) clientpath = findExecutable("festival_client");
380 insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
381 tr("Path to Festival client:"),clientpath,EncTtsSetting::eBROWSEBTN));
383 // voice
384 EncTtsSetting* setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,
385 tr("Voice:"),RbSettings::subValue("festival",RbSettings::TtsVoice),getVoiceList(exepath),EncTtsSetting::eREFRESHBTN);
386 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
387 connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription()));
388 insertSetting(eVOICE,setting);
390 //voice description
391 setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
392 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
393 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription()));
394 insertSetting(eVOICEDESC,setting);
397 void TTSFestival::saveSettings()
399 //save settings in user config
400 RbSettings::setSubValue("festival-server",RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
401 RbSettings::setSubValue("festival-client",RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
402 RbSettings::setSubValue("festival",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
404 RbSettings::sync();
407 void TTSFestival::updateVoiceDescription()
409 // get voice Info with current voice and path
410 QString info = getVoiceInfo(getSetting(eVOICE)->current().toString(),getSetting(eSERVERPATH)->current().toString());
411 getSetting(eVOICEDESC)->setCurrent(info);
414 void TTSFestival::clearVoiceDescription()
416 getSetting(eVOICEDESC)->setCurrent("");
419 void TTSFestival::updateVoiceList()
421 QStringList voiceList = getVoiceList(getSetting(eSERVERPATH)->current().toString());
422 getSetting(eVOICE)->setList(voiceList);
423 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
424 else getSetting(eVOICE)->setCurrent("");
427 void TTSFestival::startServer(QString path)
429 if(!configOk())
430 return;
432 if(path == "")
433 path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
435 serverProcess.start(QString("%1 --server").arg(path));
436 serverProcess.waitForStarted();
438 queryServer("(getpid)",300,path);
439 if(serverProcess.state() == QProcess::Running)
440 qDebug() << "Festival is up and running";
441 else
442 qDebug() << "Festival failed to start";
445 void TTSFestival::ensureServerRunning(QString path)
447 if(serverProcess.state() != QProcess::Running)
449 startServer(path);
453 bool TTSFestival::start(QString* errStr)
455 (void) errStr;
456 ensureServerRunning();
457 if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
458 queryServer(QString("(voice.select '%1)")
459 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString()));
461 return true;
464 bool TTSFestival::stop()
466 serverProcess.terminate();
467 serverProcess.kill();
469 return true;
472 TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
474 qDebug() << text << "->" << wavfile;
476 QString path = RbSettings::subValue("festival-client",RbSettings::TtsPath).toString();
477 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(path).arg(wavfile);
478 qDebug() << cmd;
480 QProcess clientProcess;
481 clientProcess.start(cmd);
482 clientProcess.write(QString("%1.\n").arg(text).toAscii());
483 clientProcess.waitForBytesWritten();
484 clientProcess.closeWriteChannel();
485 clientProcess.waitForReadyRead();
486 QString response = clientProcess.readAll();
487 response = response.trimmed();
488 if(!response.contains("Utterance"))
490 qDebug() << "Could not voice string: " << response;
491 *errStr = tr("engine could not voice string");
492 return Warning;
493 /* do not stop the voicing process because of a single string
494 TODO: needs proper settings */
496 clientProcess.closeReadChannel(QProcess::StandardError);
497 clientProcess.closeReadChannel(QProcess::StandardOutput);
498 clientProcess.terminate();
499 clientProcess.kill();
501 return NoError;
504 bool TTSFestival::configOk()
506 QString serverPath = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
507 QString clientPath = RbSettings::subValue("festival-client",RbSettings::TtsVoice).toString();
509 bool ret = QFileInfo(serverPath).isExecutable() &&
510 QFileInfo(clientPath).isExecutable();
511 if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0 && voices.size() > 0)
512 ret = ret && (voices.indexOf(RbSettings::subValue("festival",RbSettings::TtsVoice).toString()) != -1);
513 return ret;
516 QStringList TTSFestival::getVoiceList(QString path)
518 if(!configOk())
519 return QStringList();
521 if(voices.size() > 0)
523 qDebug() << "Using voice cache";
524 return voices;
527 QString response = queryServer("(voice.list)",3000,path);
529 // get the 2nd line. It should be (<voice_name>, <voice_name>)
530 response = response.mid(response.indexOf('\n') + 1, -1);
531 response = response.left(response.indexOf('\n')).trimmed();
533 voices = response.mid(1, response.size()-2).split(' ');
535 voices.sort();
536 if (voices.size() == 1 && voices[0].size() == 0)
537 voices.removeAt(0);
538 if (voices.size() > 0)
539 qDebug() << "Voices: " << voices;
540 else
541 qDebug() << "No voices.";
543 return voices;
546 QString TTSFestival::getVoiceInfo(QString voice,QString path)
548 if(!configOk())
549 return "";
551 if(!getVoiceList().contains(voice))
552 return "";
554 if(voiceDescriptions.contains(voice))
555 return voiceDescriptions[voice];
557 QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000,path);
559 if (response == "")
561 voiceDescriptions[voice]=tr("No description available");
563 else
565 response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard));
566 qDebug() << "voiceInfo w/o descr: " << response;
567 response = response.remove(')');
568 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
569 responseLines.removeAt(0); // the voice name itself
571 QString description;
572 foreach(QString line, responseLines)
574 line = line.remove('(');
575 line = line.simplified();
577 line[0] = line[0].toUpper(); // capitalize the key
579 int firstSpace = line.indexOf(' ');
580 if (firstSpace > 0)
582 line = line.insert(firstSpace, ':'); // add a colon between the key and the value
583 line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value
586 description += line + "\n";
588 voiceDescriptions[voice] = description.trimmed();
591 return voiceDescriptions[voice];
594 QString TTSFestival::queryServer(QString query, int timeout,QString path)
596 if(!configOk())
597 return "";
599 // this operation could take some time
600 emit busy();
602 ensureServerRunning(path);
604 qDebug() << "queryServer with " << query;
605 QString response;
607 QDateTime endTime;
608 if(timeout > 0)
609 endTime = QDateTime::currentDateTime().addMSecs(timeout);
611 /* Festival is *extremely* unreliable. Although at this
612 * point we are sure that SIOD is accepting commands,
613 * we might end up with an empty response. Hence, the loop.
615 while(true)
617 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
618 QTcpSocket socket;
620 socket.connectToHost("localhost", 1314);
621 socket.waitForConnected();
623 if(socket.state() == QAbstractSocket::ConnectedState)
625 socket.write(QString("%1\n").arg(query).toAscii());
626 socket.waitForBytesWritten();
627 socket.waitForReadyRead();
629 response = socket.readAll().trimmed();
631 if (response != "LP" && response != "")
632 break;
634 socket.abort();
635 socket.disconnectFromHost();
637 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
639 emit busyEnd();
640 return "";
642 /* make sure we wait a little as we don't want to flood the server with requests */
643 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
644 while(QDateTime::currentDateTime() < tmpEndTime)
645 QCoreApplication::processEvents(QEventLoop::AllEvents);
647 if(response == "nil")
649 emit busyEnd();
650 return "";
653 QStringList lines = response.split('\n');
654 if(lines.size() > 2)
656 lines.removeFirst();
657 lines.removeLast();
659 else
660 qDebug() << "Response too short: " << response;
662 emit busyEnd();
663 return lines.join("\n");