Use a nice apostroph
[kugel-rb.git] / rbutil / rbutilqt / base / ttsfestival.cpp
blob7cad16d3ddb316740dd21b0f0ad6ca969365eca0
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 "ttsfestival.h"
21 #include "utils.h"
22 #include "rbsettings.h"
24 TTSFestival::~TTSFestival()
26 qDebug() << "[Festival] Destroying instance";
27 stop();
30 void TTSFestival::generateSettings()
32 // server path
33 QString exepath = RbSettings::subValue("festival-server",
34 RbSettings::TtsPath).toString();
35 if(exepath == "" ) exepath = Utils::findExecutable("festival");
36 insertSetting(eSERVERPATH,new EncTtsSetting(this,
37 EncTtsSetting::eSTRING, "Path to Festival server:",
38 exepath,EncTtsSetting::eBROWSEBTN));
40 // client path
41 QString clientpath = RbSettings::subValue("festival-client",
42 RbSettings::TtsPath).toString();
43 if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client");
44 insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
45 tr("Path to Festival client:"),
46 clientpath,EncTtsSetting::eBROWSEBTN));
48 // voice
49 EncTtsSetting* setting = new EncTtsSetting(this,
50 EncTtsSetting::eSTRINGLIST, tr("Voice:"),
51 RbSettings::subValue("festival", RbSettings::TtsVoice),
52 getVoiceList(), EncTtsSetting::eREFRESHBTN);
53 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
54 connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription()));
55 insertSetting(eVOICE,setting);
57 //voice description
58 setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
59 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
60 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription()));
61 insertSetting(eVOICEDESC,setting);
64 void TTSFestival::saveSettings()
66 //save settings in user config
67 RbSettings::setSubValue("festival-server",
68 RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
69 RbSettings::setSubValue("festival-client",
70 RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
71 RbSettings::setSubValue("festival",
72 RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
74 RbSettings::sync();
77 void TTSFestival::updateVoiceDescription()
79 // get voice Info with current voice and path
80 currentPath = getSetting(eSERVERPATH)->current().toString();
81 QString info = getVoiceInfo(getSetting(eVOICE)->current().toString());
82 currentPath = "";
84 getSetting(eVOICEDESC)->setCurrent(info);
87 void TTSFestival::clearVoiceDescription()
89 getSetting(eVOICEDESC)->setCurrent("");
92 void TTSFestival::updateVoiceList()
94 currentPath = getSetting(eSERVERPATH)->current().toString();
95 QStringList voiceList = getVoiceList();
96 currentPath = "";
98 getSetting(eVOICE)->setList(voiceList);
99 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
100 else getSetting(eVOICE)->setCurrent("");
103 void TTSFestival::startServer()
105 if(!configOk())
106 return;
108 if(serverProcess.state() != QProcess::Running)
110 QString path;
111 /* currentPath is set by the GUI - if it's set, it is the currently set
112 path in the configuration GUI; if it's not set, use the saved path */
113 if (currentPath == "")
114 path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
115 else
116 path = currentPath;
118 serverProcess.start(QString("%1 --server").arg(path));
119 serverProcess.waitForStarted();
121 /* A friendlier version of a spinlock */
122 while (serverProcess.pid() == 0 && serverProcess.state() != QProcess::Running)
123 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
125 if(serverProcess.state() == QProcess::Running)
126 qDebug() << "[Festival] Server is up and running";
127 else
128 qDebug() << "[Festival] Server failed to start, state: " << serverProcess.state();
132 bool TTSFestival::ensureServerRunning()
134 if(serverProcess.state() != QProcess::Running)
136 startServer();
138 return serverProcess.state() == QProcess::Running;
141 bool TTSFestival::start(QString* errStr)
143 qDebug() << "[Festival] Starting server with voice " << RbSettings::subValue("festival", RbSettings::TtsVoice).toString();
145 bool running = ensureServerRunning();
146 if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
148 /* There's no harm in using both methods to set the voice .. */
149 QString voiceSelect = QString("(voice.select '%1)\n")
150 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString());
151 queryServer(voiceSelect, 3000);
153 if(prologFile.open())
155 prologFile.write(voiceSelect.toAscii());
156 prologFile.close();
157 prologPath = QFileInfo(prologFile).absoluteFilePath();
158 qDebug() << "[Festival] Prolog created at " << prologPath;
163 if (!running)
164 (*errStr) = "Festival could not be started";
165 return running;
168 bool TTSFestival::stop()
170 serverProcess.terminate();
171 serverProcess.kill();
173 return true;
176 TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
178 qDebug() << "[Festival] Voicing " << text << "->" << wavfile;
180 QString path = RbSettings::subValue("festival-client",
181 RbSettings::TtsPath).toString();
182 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp"
183 " --output \"%2\" --prolog \"%3\" - ").arg(path).arg(wavfile).arg(prologPath);
184 qDebug() << "[Festival] Client cmd: " << cmd;
186 QProcess clientProcess;
187 clientProcess.start(cmd);
188 clientProcess.write(QString("%1.\n").arg(text).toAscii());
189 clientProcess.waitForBytesWritten();
190 clientProcess.closeWriteChannel();
191 clientProcess.waitForReadyRead();
192 QString response = clientProcess.readAll();
193 response = response.trimmed();
194 if(!response.contains("Utterance"))
196 qDebug() << "[Festival] Could not voice string: " << response;
197 *errStr = tr("engine could not voice string");
198 return Warning;
199 /* do not stop the voicing process because of a single string
200 TODO: needs proper settings */
202 clientProcess.closeReadChannel(QProcess::StandardError);
203 clientProcess.closeReadChannel(QProcess::StandardOutput);
204 clientProcess.terminate();
205 clientProcess.kill();
207 return NoError;
210 bool TTSFestival::configOk()
212 bool ret;
213 if (currentPath == "")
215 QString serverPath = RbSettings::subValue("festival-server",
216 RbSettings::TtsPath).toString();
217 QString clientPath = RbSettings::subValue("festival-client",
218 RbSettings::TtsPath).toString();
220 ret = QFileInfo(serverPath).isExecutable() &&
221 QFileInfo(clientPath).isExecutable();
222 if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0
223 && voices.size() > 0)
224 ret = ret && (voices.indexOf(RbSettings::subValue("festival",
225 RbSettings::TtsVoice).toString()) != -1);
227 else /* If we're currently configuring the server, we need to know that
228 the entered path is valid */
229 ret = QFileInfo(currentPath).isExecutable();
231 return ret;
234 QStringList TTSFestival::getVoiceList()
236 if(!configOk())
237 return QStringList();
239 if(voices.size() > 0)
241 qDebug() << "[Festival] Using voice cache";
242 return voices;
245 QString response = queryServer("(voice.list)", 10000);
247 // get the 2nd line. It should be (<voice_name>, <voice_name>)
248 response = response.mid(response.indexOf('\n') + 1, -1);
249 response = response.left(response.indexOf('\n')).trimmed();
251 voices = response.mid(1, response.size()-2).split(' ');
253 voices.sort();
254 if (voices.size() == 1 && voices[0].size() == 0)
255 voices.removeAt(0);
256 if (voices.size() > 0)
257 qDebug() << "[Festival] Voices: " << voices;
258 else
259 qDebug() << "[Festival] No voices. Response was: " << response;
261 return voices;
264 QString TTSFestival::getVoiceInfo(QString voice)
266 if(!configOk())
267 return "";
269 if(!getVoiceList().contains(voice))
270 return "";
272 if(voiceDescriptions.contains(voice))
273 return voiceDescriptions[voice];
275 QString response = queryServer(QString("(voice.description '%1)").arg(voice),
276 10000);
278 if (response == "")
280 voiceDescriptions[voice]=tr("No description available");
282 else
284 response = response.remove(QRegExp("(description \"*\")",
285 Qt::CaseInsensitive, QRegExp::Wildcard));
286 qDebug() << "[Festival] voiceInfo w/o descr: " << response;
287 response = response.remove(')');
288 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
289 responseLines.removeAt(0); // the voice name itself
291 QString description;
292 foreach(QString line, responseLines)
294 line = line.remove('(');
295 line = line.simplified();
297 line[0] = line[0].toUpper(); // capitalize the key
299 int firstSpace = line.indexOf(' ');
300 if (firstSpace > 0)
302 // add a colon between the key and the value
303 line = line.insert(firstSpace, ':');
304 // capitalize the value
305 line[firstSpace+2] = line[firstSpace+2].toUpper();
308 description += line + "\n";
310 voiceDescriptions[voice] = description.trimmed();
313 return voiceDescriptions[voice];
316 QString TTSFestival::queryServer(QString query, int timeout)
318 if(!configOk())
319 return "";
321 // this operation could take some time
322 emit busy();
324 qDebug() << "[Festival] queryServer with " << query;
326 if (!ensureServerRunning())
328 qDebug() << "[Festival] queryServer: ensureServerRunning failed";
329 emit busyEnd();
330 return "";
333 QString response;
335 QDateTime endTime;
336 if(timeout > 0)
337 endTime = QDateTime::currentDateTime().addMSecs(timeout);
339 /* Festival is *extremely* unreliable. Although at this
340 * point we are sure that SIOD is accepting commands,
341 * we might end up with an empty response. Hence, the loop.
343 while(true)
345 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
346 QTcpSocket socket;
348 socket.connectToHost("localhost", 1314);
349 socket.waitForConnected();
351 if(socket.state() == QAbstractSocket::ConnectedState)
353 socket.write(QString("%1\n").arg(query).toAscii());
354 socket.waitForBytesWritten();
355 socket.waitForReadyRead();
357 response = socket.readAll().trimmed();
359 if (response != "LP" && response != "")
360 break;
362 socket.abort();
363 socket.disconnectFromHost();
365 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
367 emit busyEnd();
368 return "";
370 /* make sure we wait a little as we don't want to flood the server
371 * with requests */
372 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
373 while(QDateTime::currentDateTime() < tmpEndTime)
374 QCoreApplication::processEvents(QEventLoop::AllEvents);
376 if(response == "nil")
378 emit busyEnd();
379 return "";
382 QStringList lines = response.split('\n');
383 if(lines.size() > 2)
385 lines.removeFirst(); /* should be LP */
386 lines.removeLast(); /* should be ft_StUfF_keyOK */
388 else
389 qDebug() << "[Festival] Response too short: " << response;
391 emit busyEnd();
392 return lines.join("\n");