Handle language change events in widgets.
[maemo-rb.git] / rbutil / rbutilqt / base / ttsfestival.cpp
blob9ba6275ea649bbcc0851fabe0f9f2cb31d5bbd71
1 /***************************************************************************
2 * __________ __ ___.
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
7 * \/ \/ \/ \/ \/
9 * Copyright (C) 2007 by Dominik Wenger
11 * All files in this archive are subject to the GNU General Public License.
12 * See the file COPYING in the source tree root for full license agreement.
14 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
15 * KIND, either express or implied.
17 ****************************************************************************/
19 #include <QtCore>
20 #include <QTcpSocket>
22 #include "ttsfestival.h"
23 #include "utils.h"
24 #include "rbsettings.h"
26 TTSFestival::~TTSFestival()
28 qDebug() << "[Festival] Destroying instance";
29 stop();
32 TTSBase::Capabilities TTSFestival::capabilities()
34 return RunInParallel;
37 void TTSFestival::generateSettings()
39 // server path
40 QString exepath = RbSettings::subValue("festival-server",
41 RbSettings::TtsPath).toString();
42 if(exepath == "" ) exepath = Utils::findExecutable("festival");
43 insertSetting(eSERVERPATH,new EncTtsSetting(this,
44 EncTtsSetting::eSTRING, "Path to Festival server:",
45 exepath,EncTtsSetting::eBROWSEBTN));
47 // client path
48 QString clientpath = RbSettings::subValue("festival-client",
49 RbSettings::TtsPath).toString();
50 if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client");
51 insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
52 tr("Path to Festival client:"),
53 clientpath,EncTtsSetting::eBROWSEBTN));
55 // voice
56 EncTtsSetting* setting = new EncTtsSetting(this,
57 EncTtsSetting::eSTRINGLIST, tr("Voice:"),
58 RbSettings::subValue("festival", RbSettings::TtsVoice),
59 getVoiceList(), EncTtsSetting::eREFRESHBTN);
60 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
61 connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription()));
62 insertSetting(eVOICE,setting);
64 //voice description
65 setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
66 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
67 connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription()));
68 insertSetting(eVOICEDESC,setting);
71 void TTSFestival::saveSettings()
73 //save settings in user config
74 RbSettings::setSubValue("festival-server",
75 RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
76 RbSettings::setSubValue("festival-client",
77 RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
78 RbSettings::setSubValue("festival",
79 RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
81 RbSettings::sync();
84 void TTSFestival::updateVoiceDescription()
86 // get voice Info with current voice and path
87 currentPath = getSetting(eSERVERPATH)->current().toString();
88 QString info = getVoiceInfo(getSetting(eVOICE)->current().toString());
89 currentPath = "";
91 getSetting(eVOICEDESC)->setCurrent(info);
94 void TTSFestival::clearVoiceDescription()
96 getSetting(eVOICEDESC)->setCurrent("");
99 void TTSFestival::updateVoiceList()
101 currentPath = getSetting(eSERVERPATH)->current().toString();
102 QStringList voiceList = getVoiceList();
103 currentPath = "";
105 getSetting(eVOICE)->setList(voiceList);
106 if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
107 else getSetting(eVOICE)->setCurrent("");
110 void TTSFestival::startServer()
112 if(!configOk())
113 return;
115 if(serverProcess.state() != QProcess::Running)
117 QString path;
118 /* currentPath is set by the GUI - if it's set, it is the currently set
119 path in the configuration GUI; if it's not set, use the saved path */
120 if (currentPath == "")
121 path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
122 else
123 path = currentPath;
125 serverProcess.start(QString("%1 --server").arg(path));
126 serverProcess.waitForStarted();
128 /* A friendlier version of a spinlock */
129 while (serverProcess.pid() == 0 && serverProcess.state() != QProcess::Running)
130 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
132 if(serverProcess.state() == QProcess::Running)
133 qDebug() << "[Festival] Server is up and running";
134 else
135 qDebug() << "[Festival] Server failed to start, state: " << serverProcess.state();
139 bool TTSFestival::ensureServerRunning()
141 if(serverProcess.state() != QProcess::Running)
143 startServer();
145 return serverProcess.state() == QProcess::Running;
148 bool TTSFestival::start(QString* errStr)
150 qDebug() << "[Festival] Starting server with voice " << RbSettings::subValue("festival", RbSettings::TtsVoice).toString();
152 bool running = ensureServerRunning();
153 if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
155 /* There's no harm in using both methods to set the voice .. */
156 QString voiceSelect = QString("(voice.select '%1)\n")
157 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString());
158 queryServer(voiceSelect, 3000);
160 if(prologFile.open())
162 prologFile.write(voiceSelect.toAscii());
163 prologFile.close();
164 prologPath = QFileInfo(prologFile).absoluteFilePath();
165 qDebug() << "[Festival] Prolog created at " << prologPath;
170 if (!running)
171 (*errStr) = "Festival could not be started";
172 return running;
175 bool TTSFestival::stop()
177 serverProcess.terminate();
178 serverProcess.kill();
180 return true;
183 TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr)
185 qDebug() << "[Festival] Voicing " << text << "->" << wavfile;
187 QString path = RbSettings::subValue("festival-client",
188 RbSettings::TtsPath).toString();
189 QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp"
190 " --output \"%2\" --prolog \"%3\" - ").arg(path).arg(wavfile).arg(prologPath);
191 qDebug() << "[Festival] Client cmd: " << cmd;
193 QProcess clientProcess;
194 clientProcess.start(cmd);
195 clientProcess.write(QString("%1.\n").arg(text).toAscii());
196 clientProcess.waitForBytesWritten();
197 clientProcess.closeWriteChannel();
198 clientProcess.waitForReadyRead();
199 QString response = clientProcess.readAll();
200 response = response.trimmed();
201 if(!response.contains("Utterance"))
203 qDebug() << "[Festival] Could not voice string: " << response;
204 *errStr = tr("engine could not voice string");
205 return Warning;
206 /* do not stop the voicing process because of a single string
207 TODO: needs proper settings */
209 clientProcess.closeReadChannel(QProcess::StandardError);
210 clientProcess.closeReadChannel(QProcess::StandardOutput);
211 clientProcess.terminate();
212 clientProcess.kill();
214 return NoError;
217 bool TTSFestival::configOk()
219 bool ret;
220 if (currentPath == "")
222 QString serverPath = RbSettings::subValue("festival-server",
223 RbSettings::TtsPath).toString();
224 QString clientPath = RbSettings::subValue("festival-client",
225 RbSettings::TtsPath).toString();
227 ret = QFileInfo(serverPath).isExecutable() &&
228 QFileInfo(clientPath).isExecutable();
229 if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0
230 && voices.size() > 0)
231 ret = ret && (voices.indexOf(RbSettings::subValue("festival",
232 RbSettings::TtsVoice).toString()) != -1);
234 else /* If we're currently configuring the server, we need to know that
235 the entered path is valid */
236 ret = QFileInfo(currentPath).isExecutable();
238 return ret;
241 QStringList TTSFestival::getVoiceList()
243 if(!configOk())
244 return QStringList();
246 if(voices.size() > 0)
248 qDebug() << "[Festival] Using voice cache";
249 return voices;
252 QString response = queryServer("(voice.list)", 10000);
254 // get the 2nd line. It should be (<voice_name>, <voice_name>)
255 response = response.mid(response.indexOf('\n') + 1, -1);
256 response = response.left(response.indexOf('\n')).trimmed();
258 voices = response.mid(1, response.size()-2).split(' ');
260 voices.sort();
261 if (voices.size() == 1 && voices[0].size() == 0)
262 voices.removeAt(0);
263 if (voices.size() > 0)
264 qDebug() << "[Festival] Voices: " << voices;
265 else
266 qDebug() << "[Festival] No voices. Response was: " << response;
268 return voices;
271 QString TTSFestival::getVoiceInfo(QString voice)
273 if(!configOk())
274 return "";
276 if(!getVoiceList().contains(voice))
277 return "";
279 if(voiceDescriptions.contains(voice))
280 return voiceDescriptions[voice];
282 QString response = queryServer(QString("(voice.description '%1)").arg(voice),
283 10000);
285 if (response == "")
287 voiceDescriptions[voice]=tr("No description available");
289 else
291 response = response.remove(QRegExp("(description \"*\")",
292 Qt::CaseInsensitive, QRegExp::Wildcard));
293 qDebug() << "[Festival] voiceInfo w/o descr: " << response;
294 response = response.remove(')');
295 QStringList responseLines = response.split('(', QString::SkipEmptyParts);
296 responseLines.removeAt(0); // the voice name itself
298 QString description;
299 foreach(QString line, responseLines)
301 line = line.remove('(');
302 line = line.simplified();
304 line[0] = line[0].toUpper(); // capitalize the key
306 int firstSpace = line.indexOf(' ');
307 if (firstSpace > 0)
309 // add a colon between the key and the value
310 line = line.insert(firstSpace, ':');
311 // capitalize the value
312 line[firstSpace+2] = line[firstSpace+2].toUpper();
315 description += line + "\n";
317 voiceDescriptions[voice] = description.trimmed();
320 return voiceDescriptions[voice];
323 QString TTSFestival::queryServer(QString query, int timeout)
325 if(!configOk())
326 return "";
328 // this operation could take some time
329 emit busy();
331 qDebug() << "[Festival] queryServer with " << query;
333 if (!ensureServerRunning())
335 qDebug() << "[Festival] queryServer: ensureServerRunning failed";
336 emit busyEnd();
337 return "";
340 QString response;
342 QDateTime endTime;
343 if(timeout > 0)
344 endTime = QDateTime::currentDateTime().addMSecs(timeout);
346 /* Festival is *extremely* unreliable. Although at this
347 * point we are sure that SIOD is accepting commands,
348 * we might end up with an empty response. Hence, the loop.
350 while(true)
352 QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
353 QTcpSocket socket;
355 socket.connectToHost("localhost", 1314);
356 socket.waitForConnected();
358 if(socket.state() == QAbstractSocket::ConnectedState)
360 socket.write(QString("%1\n").arg(query).toAscii());
361 socket.waitForBytesWritten();
362 socket.waitForReadyRead();
364 response = socket.readAll().trimmed();
366 if (response != "LP" && response != "")
367 break;
369 socket.abort();
370 socket.disconnectFromHost();
372 if(timeout > 0 && QDateTime::currentDateTime() >= endTime)
374 emit busyEnd();
375 return "";
377 /* make sure we wait a little as we don't want to flood the server
378 * with requests */
379 QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
380 while(QDateTime::currentDateTime() < tmpEndTime)
381 QCoreApplication::processEvents(QEventLoop::AllEvents);
383 if(response == "nil")
385 emit busyEnd();
386 return "";
389 QStringList lines = response.split('\n');
390 if(lines.size() > 2)
392 lines.removeFirst(); /* should be LP */
393 lines.removeLast(); /* should be ft_StUfF_keyOK */
395 else
396 qDebug() << "[Festival] Response too short: " << response;
398 emit busyEnd();
399 return lines.join("\n");