1 /***************************************************************************
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
9 * Copyright (C) 2007 by Dominik Wenger
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"
22 #include "rbsettings.h"
24 TTSFestival::~TTSFestival()
26 qDebug() << "[Festival] Destroying instance";
30 void TTSFestival::generateSettings()
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
));
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
));
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
);
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());
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());
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();
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()
108 if(serverProcess
.state() != QProcess::Running
)
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();
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";
128 qDebug() << "[Festival] Server failed to start, state: " << serverProcess
.state();
132 bool TTSFestival::ensureServerRunning()
134 if(serverProcess
.state() != QProcess::Running
)
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());
157 prologPath
= QFileInfo(prologFile
).absoluteFilePath();
158 qDebug() << "[Festival] Prolog created at " << prologPath
;
164 (*errStr
) = "Festival could not be started";
168 bool TTSFestival::stop()
170 serverProcess
.terminate();
171 serverProcess
.kill();
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");
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();
210 bool TTSFestival::configOk()
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();
234 QStringList
TTSFestival::getVoiceList()
237 return QStringList();
239 if(voices
.size() > 0)
241 qDebug() << "[Festival] Using voice cache";
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(' ');
254 if (voices
.size() == 1 && voices
[0].size() == 0)
256 if (voices
.size() > 0)
257 qDebug() << "[Festival] Voices: " << voices
;
259 qDebug() << "[Festival] No voices. Response was: " << response
;
264 QString
TTSFestival::getVoiceInfo(QString voice
)
269 if(!getVoiceList().contains(voice
))
272 if(voiceDescriptions
.contains(voice
))
273 return voiceDescriptions
[voice
];
275 QString response
= queryServer(QString("(voice.description '%1)").arg(voice
),
280 voiceDescriptions
[voice
]=tr("No description available");
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
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(' ');
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
)
321 // this operation could take some time
324 qDebug() << "[Festival] queryServer with " << query
;
326 if (!ensureServerRunning())
328 qDebug() << "[Festival] queryServer: ensureServerRunning failed";
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.
345 QCoreApplication::processEvents(QEventLoop::AllEvents
, 50);
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
!= "")
363 socket
.disconnectFromHost();
365 if(timeout
> 0 && QDateTime::currentDateTime() >= endTime
)
370 /* make sure we wait a little as we don't want to flood the server
372 QDateTime tmpEndTime
= QDateTime::currentDateTime().addMSecs(500);
373 while(QDateTime::currentDateTime() < tmpEndTime
)
374 QCoreApplication::processEvents(QEventLoop::AllEvents
);
376 if(response
== "nil")
382 QStringList lines
= response
.split('\n');
385 lines
.removeFirst(); /* should be LP */
386 lines
.removeLast(); /* should be ft_StUfF_keyOK */
389 qDebug() << "[Festival] Response too short: " << response
;
392 return lines
.join("\n");