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 TTSBase::Capabilities
TTSFestival::capabilities()
35 void TTSFestival::generateSettings()
38 QString exepath
= RbSettings::subValue("festival-server",
39 RbSettings::TtsPath
).toString();
40 if(exepath
== "" ) exepath
= Utils::findExecutable("festival");
41 insertSetting(eSERVERPATH
,new EncTtsSetting(this,
42 EncTtsSetting::eSTRING
, "Path to Festival server:",
43 exepath
,EncTtsSetting::eBROWSEBTN
));
46 QString clientpath
= RbSettings::subValue("festival-client",
47 RbSettings::TtsPath
).toString();
48 if(clientpath
== "" ) clientpath
= Utils::findExecutable("festival_client");
49 insertSetting(eCLIENTPATH
,new EncTtsSetting(this,EncTtsSetting::eSTRING
,
50 tr("Path to Festival client:"),
51 clientpath
,EncTtsSetting::eBROWSEBTN
));
54 EncTtsSetting
* setting
= new EncTtsSetting(this,
55 EncTtsSetting::eSTRINGLIST
, tr("Voice:"),
56 RbSettings::subValue("festival", RbSettings::TtsVoice
),
57 getVoiceList(), EncTtsSetting::eREFRESHBTN
);
58 connect(setting
,SIGNAL(refresh()),this,SLOT(updateVoiceList()));
59 connect(setting
,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription()));
60 insertSetting(eVOICE
,setting
);
63 setting
= new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING
,
64 tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN
);
65 connect(setting
,SIGNAL(refresh()),this,SLOT(updateVoiceDescription()));
66 insertSetting(eVOICEDESC
,setting
);
69 void TTSFestival::saveSettings()
71 //save settings in user config
72 RbSettings::setSubValue("festival-server",
73 RbSettings::TtsPath
,getSetting(eSERVERPATH
)->current().toString());
74 RbSettings::setSubValue("festival-client",
75 RbSettings::TtsPath
,getSetting(eCLIENTPATH
)->current().toString());
76 RbSettings::setSubValue("festival",
77 RbSettings::TtsVoice
,getSetting(eVOICE
)->current().toString());
82 void TTSFestival::updateVoiceDescription()
84 // get voice Info with current voice and path
85 currentPath
= getSetting(eSERVERPATH
)->current().toString();
86 QString info
= getVoiceInfo(getSetting(eVOICE
)->current().toString());
89 getSetting(eVOICEDESC
)->setCurrent(info
);
92 void TTSFestival::clearVoiceDescription()
94 getSetting(eVOICEDESC
)->setCurrent("");
97 void TTSFestival::updateVoiceList()
99 currentPath
= getSetting(eSERVERPATH
)->current().toString();
100 QStringList voiceList
= getVoiceList();
103 getSetting(eVOICE
)->setList(voiceList
);
104 if(voiceList
.size() > 0) getSetting(eVOICE
)->setCurrent(voiceList
.at(0));
105 else getSetting(eVOICE
)->setCurrent("");
108 void TTSFestival::startServer()
113 if(serverProcess
.state() != QProcess::Running
)
116 /* currentPath is set by the GUI - if it's set, it is the currently set
117 path in the configuration GUI; if it's not set, use the saved path */
118 if (currentPath
== "")
119 path
= RbSettings::subValue("festival-server",RbSettings::TtsPath
).toString();
123 serverProcess
.start(QString("%1 --server").arg(path
));
124 serverProcess
.waitForStarted();
126 /* A friendlier version of a spinlock */
127 while (serverProcess
.pid() == 0 && serverProcess
.state() != QProcess::Running
)
128 QCoreApplication::processEvents(QEventLoop::AllEvents
, 50);
130 if(serverProcess
.state() == QProcess::Running
)
131 qDebug() << "[Festival] Server is up and running";
133 qDebug() << "[Festival] Server failed to start, state: " << serverProcess
.state();
137 bool TTSFestival::ensureServerRunning()
139 if(serverProcess
.state() != QProcess::Running
)
143 return serverProcess
.state() == QProcess::Running
;
146 bool TTSFestival::start(QString
* errStr
)
148 qDebug() << "[Festival] Starting server with voice " << RbSettings::subValue("festival", RbSettings::TtsVoice
).toString();
150 bool running
= ensureServerRunning();
151 if (!RbSettings::subValue("festival",RbSettings::TtsVoice
).toString().isEmpty())
153 /* There's no harm in using both methods to set the voice .. */
154 QString voiceSelect
= QString("(voice.select '%1)\n")
155 .arg(RbSettings::subValue("festival", RbSettings::TtsVoice
).toString());
156 queryServer(voiceSelect
, 3000);
158 if(prologFile
.open())
160 prologFile
.write(voiceSelect
.toAscii());
162 prologPath
= QFileInfo(prologFile
).absoluteFilePath();
163 qDebug() << "[Festival] Prolog created at " << prologPath
;
169 (*errStr
) = "Festival could not be started";
173 bool TTSFestival::stop()
175 serverProcess
.terminate();
176 serverProcess
.kill();
181 TTSStatus
TTSFestival::voice(QString text
, QString wavfile
, QString
* errStr
)
183 qDebug() << "[Festival] Voicing " << text
<< "->" << wavfile
;
185 QString path
= RbSettings::subValue("festival-client",
186 RbSettings::TtsPath
).toString();
187 QString cmd
= QString("%1 --server localhost --otype riff --ttw --withlisp"
188 " --output \"%2\" --prolog \"%3\" - ").arg(path
).arg(wavfile
).arg(prologPath
);
189 qDebug() << "[Festival] Client cmd: " << cmd
;
191 QProcess clientProcess
;
192 clientProcess
.start(cmd
);
193 clientProcess
.write(QString("%1.\n").arg(text
).toAscii());
194 clientProcess
.waitForBytesWritten();
195 clientProcess
.closeWriteChannel();
196 clientProcess
.waitForReadyRead();
197 QString response
= clientProcess
.readAll();
198 response
= response
.trimmed();
199 if(!response
.contains("Utterance"))
201 qDebug() << "[Festival] Could not voice string: " << response
;
202 *errStr
= tr("engine could not voice string");
204 /* do not stop the voicing process because of a single string
205 TODO: needs proper settings */
207 clientProcess
.closeReadChannel(QProcess::StandardError
);
208 clientProcess
.closeReadChannel(QProcess::StandardOutput
);
209 clientProcess
.terminate();
210 clientProcess
.kill();
215 bool TTSFestival::configOk()
218 if (currentPath
== "")
220 QString serverPath
= RbSettings::subValue("festival-server",
221 RbSettings::TtsPath
).toString();
222 QString clientPath
= RbSettings::subValue("festival-client",
223 RbSettings::TtsPath
).toString();
225 ret
= QFileInfo(serverPath
).isExecutable() &&
226 QFileInfo(clientPath
).isExecutable();
227 if(RbSettings::subValue("festival",RbSettings::TtsVoice
).toString().size() > 0
228 && voices
.size() > 0)
229 ret
= ret
&& (voices
.indexOf(RbSettings::subValue("festival",
230 RbSettings::TtsVoice
).toString()) != -1);
232 else /* If we're currently configuring the server, we need to know that
233 the entered path is valid */
234 ret
= QFileInfo(currentPath
).isExecutable();
239 QStringList
TTSFestival::getVoiceList()
242 return QStringList();
244 if(voices
.size() > 0)
246 qDebug() << "[Festival] Using voice cache";
250 QString response
= queryServer("(voice.list)", 10000);
252 // get the 2nd line. It should be (<voice_name>, <voice_name>)
253 response
= response
.mid(response
.indexOf('\n') + 1, -1);
254 response
= response
.left(response
.indexOf('\n')).trimmed();
256 voices
= response
.mid(1, response
.size()-2).split(' ');
259 if (voices
.size() == 1 && voices
[0].size() == 0)
261 if (voices
.size() > 0)
262 qDebug() << "[Festival] Voices: " << voices
;
264 qDebug() << "[Festival] No voices. Response was: " << response
;
269 QString
TTSFestival::getVoiceInfo(QString voice
)
274 if(!getVoiceList().contains(voice
))
277 if(voiceDescriptions
.contains(voice
))
278 return voiceDescriptions
[voice
];
280 QString response
= queryServer(QString("(voice.description '%1)").arg(voice
),
285 voiceDescriptions
[voice
]=tr("No description available");
289 response
= response
.remove(QRegExp("(description \"*\")",
290 Qt::CaseInsensitive
, QRegExp::Wildcard
));
291 qDebug() << "[Festival] voiceInfo w/o descr: " << response
;
292 response
= response
.remove(')');
293 QStringList responseLines
= response
.split('(', QString::SkipEmptyParts
);
294 responseLines
.removeAt(0); // the voice name itself
297 foreach(QString line
, responseLines
)
299 line
= line
.remove('(');
300 line
= line
.simplified();
302 line
[0] = line
[0].toUpper(); // capitalize the key
304 int firstSpace
= line
.indexOf(' ');
307 // add a colon between the key and the value
308 line
= line
.insert(firstSpace
, ':');
309 // capitalize the value
310 line
[firstSpace
+2] = line
[firstSpace
+2].toUpper();
313 description
+= line
+ "\n";
315 voiceDescriptions
[voice
] = description
.trimmed();
318 return voiceDescriptions
[voice
];
321 QString
TTSFestival::queryServer(QString query
, int timeout
)
326 // this operation could take some time
329 qDebug() << "[Festival] queryServer with " << query
;
331 if (!ensureServerRunning())
333 qDebug() << "[Festival] queryServer: ensureServerRunning failed";
342 endTime
= QDateTime::currentDateTime().addMSecs(timeout
);
344 /* Festival is *extremely* unreliable. Although at this
345 * point we are sure that SIOD is accepting commands,
346 * we might end up with an empty response. Hence, the loop.
350 QCoreApplication::processEvents(QEventLoop::AllEvents
, 50);
353 socket
.connectToHost("localhost", 1314);
354 socket
.waitForConnected();
356 if(socket
.state() == QAbstractSocket::ConnectedState
)
358 socket
.write(QString("%1\n").arg(query
).toAscii());
359 socket
.waitForBytesWritten();
360 socket
.waitForReadyRead();
362 response
= socket
.readAll().trimmed();
364 if (response
!= "LP" && response
!= "")
368 socket
.disconnectFromHost();
370 if(timeout
> 0 && QDateTime::currentDateTime() >= endTime
)
375 /* make sure we wait a little as we don't want to flood the server
377 QDateTime tmpEndTime
= QDateTime::currentDateTime().addMSecs(500);
378 while(QDateTime::currentDateTime() < tmpEndTime
)
379 QCoreApplication::processEvents(QEventLoop::AllEvents
);
381 if(response
== "nil")
387 QStringList lines
= response
.split('\n');
390 lines
.removeFirst(); /* should be LP */
391 lines
.removeLast(); /* should be ft_StUfF_keyOK */
394 qDebug() << "[Festival] Response too short: " << response
;
397 return lines
.join("\n");