1 /***************************************************************************
3 * Open \______ \ ____ ____ | | _\_ |__ _______ ___
4 * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
5 * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
6 * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
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 ****************************************************************************/
22 #include "ttsfestival.h"
24 #include "rbsettings.h"
26 TTSFestival::~TTSFestival()
28 qDebug() << "[Festival] Destroying instance";
32 TTSBase::Capabilities
TTSFestival::capabilities()
37 void TTSFestival::generateSettings()
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
));
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
));
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
);
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());
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());
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();
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()
115 if(serverProcess
.state() != QProcess::Running
)
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();
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";
135 qDebug() << "[Festival] Server failed to start, state: " << serverProcess
.state();
139 bool TTSFestival::ensureServerRunning()
141 if(serverProcess
.state() != QProcess::Running
)
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());
164 prologPath
= QFileInfo(prologFile
).absoluteFilePath();
165 qDebug() << "[Festival] Prolog created at " << prologPath
;
171 (*errStr
) = "Festival could not be started";
175 bool TTSFestival::stop()
177 serverProcess
.terminate();
178 serverProcess
.kill();
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");
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();
217 bool TTSFestival::configOk()
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();
241 QStringList
TTSFestival::getVoiceList()
244 return QStringList();
246 if(voices
.size() > 0)
248 qDebug() << "[Festival] Using voice cache";
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(' ');
261 if (voices
.size() == 1 && voices
[0].size() == 0)
263 if (voices
.size() > 0)
264 qDebug() << "[Festival] Voices: " << voices
;
266 qDebug() << "[Festival] No voices. Response was: " << response
;
271 QString
TTSFestival::getVoiceInfo(QString voice
)
276 if(!getVoiceList().contains(voice
))
279 if(voiceDescriptions
.contains(voice
))
280 return voiceDescriptions
[voice
];
282 QString response
= queryServer(QString("(voice.description '%1)").arg(voice
),
287 voiceDescriptions
[voice
]=tr("No description available");
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
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(' ');
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
)
328 // this operation could take some time
331 qDebug() << "[Festival] queryServer with " << query
;
333 if (!ensureServerRunning())
335 qDebug() << "[Festival] queryServer: ensureServerRunning failed";
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.
352 QCoreApplication::processEvents(QEventLoop::AllEvents
, 50);
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
!= "")
370 socket
.disconnectFromHost();
372 if(timeout
> 0 && QDateTime::currentDateTime() >= endTime
)
377 /* make sure we wait a little as we don't want to flood the server
379 QDateTime tmpEndTime
= QDateTime::currentDateTime().addMSecs(500);
380 while(QDateTime::currentDateTime() < tmpEndTime
)
381 QCoreApplication::processEvents(QEventLoop::AllEvents
);
383 if(response
== "nil")
389 QStringList lines
= response
.split('\n');
392 lines
.removeFirst(); /* should be LP */
393 lines
.removeLast(); /* should be ft_StUfF_keyOK */
396 qDebug() << "[Festival] Response too short: " << response
;
399 return lines
.join("\n");