diff options
author | Dominik Riebeling <Dominik.Riebeling@gmail.com> | 2021-12-15 21:04:28 +0100 |
---|---|---|
committer | Dominik Riebeling <Dominik.Riebeling@gmail.com> | 2021-12-24 18:05:53 +0100 |
commit | c876d3bbefe0dc00c27ca0c12d29da5874946962 (patch) | |
tree | 69f468a185a369b01998314bc3ecc19b70f4fcaa /utils/rbutilqt/base/ttsfestival.cpp | |
parent | 6c6f0757d7a902feb293be165d1490c42bc8e7ad (diff) | |
download | rockbox-c876d3bbefe0dc00c27ca0c12d29da5874946962.tar.gz rockbox-c876d3bbefe0dc00c27ca0c12d29da5874946962.zip |
rbutil: Merge rbutil with utils folder.
rbutil uses several components from the utils folder, and can be
considered part of utils too. Having it in a separate folder is an
arbitrary split that doesn't help anymore these days, so merge them.
This also allows other utils to easily use libtools.make without the
need to navigate to a different folder.
Change-Id: I3fc2f4de19e3e776553efb5dea5f779dfec0dc21
Diffstat (limited to 'utils/rbutilqt/base/ttsfestival.cpp')
-rw-r--r-- | utils/rbutilqt/base/ttsfestival.cpp | 412 |
1 files changed, 412 insertions, 0 deletions
diff --git a/utils/rbutilqt/base/ttsfestival.cpp b/utils/rbutilqt/base/ttsfestival.cpp new file mode 100644 index 0000000000..d0ca400909 --- /dev/null +++ b/utils/rbutilqt/base/ttsfestival.cpp | |||
@@ -0,0 +1,412 @@ | |||
1 | /*************************************************************************** | ||
2 | * __________ __ ___. | ||
3 | * Open \______ \ ____ ____ | | _\_ |__ _______ ___ | ||
4 | * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / | ||
5 | * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < | ||
6 | * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ | ||
7 | * \/ \/ \/ \/ \/ | ||
8 | * | ||
9 | * Copyright (C) 2007 by Dominik Wenger | ||
10 | * | ||
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. | ||
13 | * | ||
14 | * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY | ||
15 | * KIND, either express or implied. | ||
16 | * | ||
17 | ****************************************************************************/ | ||
18 | |||
19 | #include <QtCore> | ||
20 | #include <QTcpSocket> | ||
21 | |||
22 | #include "ttsfestival.h" | ||
23 | #include "utils.h" | ||
24 | #include "rbsettings.h" | ||
25 | #include "Logger.h" | ||
26 | |||
27 | TTSFestival::~TTSFestival() | ||
28 | { | ||
29 | LOG_INFO() << "Destroying instance"; | ||
30 | stop(); | ||
31 | } | ||
32 | |||
33 | TTSBase::Capabilities TTSFestival::capabilities() | ||
34 | { | ||
35 | return RunInParallel; | ||
36 | } | ||
37 | |||
38 | void TTSFestival::generateSettings() | ||
39 | { | ||
40 | // server path | ||
41 | QString exepath = RbSettings::subValue("festival-server", | ||
42 | RbSettings::TtsPath).toString(); | ||
43 | if(exepath == "" ) exepath = Utils::findExecutable("festival"); | ||
44 | insertSetting(eSERVERPATH,new EncTtsSetting(this, | ||
45 | EncTtsSetting::eSTRING, "Path to Festival server:", | ||
46 | exepath,EncTtsSetting::eBROWSEBTN)); | ||
47 | |||
48 | // client path | ||
49 | QString clientpath = RbSettings::subValue("festival-client", | ||
50 | RbSettings::TtsPath).toString(); | ||
51 | if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client"); | ||
52 | insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING, | ||
53 | tr("Path to Festival client:"), | ||
54 | clientpath,EncTtsSetting::eBROWSEBTN)); | ||
55 | |||
56 | // voice | ||
57 | EncTtsSetting* setting = new EncTtsSetting(this, | ||
58 | EncTtsSetting::eSTRINGLIST, tr("Voice:"), | ||
59 | RbSettings::subValue("festival", RbSettings::TtsVoice), | ||
60 | getVoiceList(), EncTtsSetting::eREFRESHBTN); | ||
61 | connect(setting, &EncTtsSetting::refresh, | ||
62 | this, &TTSFestival::updateVoiceList); | ||
63 | connect(setting, &EncTtsSetting::dataChanged, | ||
64 | this, &TTSFestival::clearVoiceDescription); | ||
65 | insertSetting(eVOICE,setting); | ||
66 | |||
67 | //voice description | ||
68 | setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING, | ||
69 | tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN); | ||
70 | connect(setting, &EncTtsSetting::refresh, | ||
71 | this, &TTSFestival::updateVoiceDescription); | ||
72 | insertSetting(eVOICEDESC,setting); | ||
73 | } | ||
74 | |||
75 | void TTSFestival::saveSettings() | ||
76 | { | ||
77 | //save settings in user config | ||
78 | RbSettings::setSubValue("festival-server", | ||
79 | RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString()); | ||
80 | RbSettings::setSubValue("festival-client", | ||
81 | RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString()); | ||
82 | RbSettings::setSubValue("festival", | ||
83 | RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); | ||
84 | |||
85 | RbSettings::sync(); | ||
86 | } | ||
87 | |||
88 | void TTSFestival::updateVoiceDescription() | ||
89 | { | ||
90 | // get voice Info with current voice and path | ||
91 | currentPath = getSetting(eSERVERPATH)->current().toString(); | ||
92 | QString info = getVoiceInfo(getSetting(eVOICE)->current().toString()); | ||
93 | currentPath = ""; | ||
94 | |||
95 | getSetting(eVOICEDESC)->setCurrent(info); | ||
96 | } | ||
97 | |||
98 | void TTSFestival::clearVoiceDescription() | ||
99 | { | ||
100 | getSetting(eVOICEDESC)->setCurrent(""); | ||
101 | } | ||
102 | |||
103 | void TTSFestival::updateVoiceList() | ||
104 | { | ||
105 | currentPath = getSetting(eSERVERPATH)->current().toString(); | ||
106 | QStringList voiceList = getVoiceList(); | ||
107 | currentPath = ""; | ||
108 | |||
109 | getSetting(eVOICE)->setList(voiceList); | ||
110 | if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); | ||
111 | else getSetting(eVOICE)->setCurrent(""); | ||
112 | } | ||
113 | |||
114 | void TTSFestival::startServer() | ||
115 | { | ||
116 | if(!configOk()) | ||
117 | return; | ||
118 | |||
119 | if(serverProcess.state() != QProcess::Running) | ||
120 | { | ||
121 | QString path; | ||
122 | /* currentPath is set by the GUI - if it's set, it is the currently set | ||
123 | path in the configuration GUI; if it's not set, use the saved path */ | ||
124 | if (currentPath == "") | ||
125 | path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString(); | ||
126 | else | ||
127 | path = currentPath; | ||
128 | |||
129 | serverProcess.start(QString("%1 --server").arg(path)); | ||
130 | serverProcess.waitForStarted(); | ||
131 | |||
132 | /* A friendlier version of a spinlock */ | ||
133 | while (serverProcess.processId() == 0 && serverProcess.state() != QProcess::Running) | ||
134 | QCoreApplication::processEvents(QEventLoop::AllEvents, 50); | ||
135 | |||
136 | if(serverProcess.state() == QProcess::Running) | ||
137 | LOG_INFO() << "Server is up and running"; | ||
138 | else | ||
139 | LOG_ERROR() << "Server failed to start, state:" | ||
140 | << serverProcess.state(); | ||
141 | } | ||
142 | } | ||
143 | |||
144 | bool TTSFestival::ensureServerRunning() | ||
145 | { | ||
146 | if(serverProcess.state() != QProcess::Running) | ||
147 | { | ||
148 | startServer(); | ||
149 | } | ||
150 | return serverProcess.state() == QProcess::Running; | ||
151 | } | ||
152 | |||
153 | bool TTSFestival::start(QString* errStr) | ||
154 | { | ||
155 | LOG_INFO() << "Starting server with voice" | ||
156 | << RbSettings::subValue("festival", RbSettings::TtsVoice).toString(); | ||
157 | |||
158 | bool running = ensureServerRunning(); | ||
159 | if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty()) | ||
160 | { | ||
161 | /* There's no harm in using both methods to set the voice .. */ | ||
162 | QString voiceSelect = QString("(voice.select '%1)\n") | ||
163 | .arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString()); | ||
164 | queryServer(voiceSelect, 3000); | ||
165 | |||
166 | if(prologFile.open()) | ||
167 | { | ||
168 | prologFile.write(voiceSelect.toLatin1()); | ||
169 | prologFile.close(); | ||
170 | prologPath = QFileInfo(prologFile).absoluteFilePath(); | ||
171 | LOG_INFO() << "Prolog created at" << prologPath; | ||
172 | } | ||
173 | |||
174 | } | ||
175 | |||
176 | if (!running) | ||
177 | (*errStr) = "Festival could not be started"; | ||
178 | return running; | ||
179 | } | ||
180 | |||
181 | bool TTSFestival::stop() | ||
182 | { | ||
183 | serverProcess.terminate(); | ||
184 | serverProcess.kill(); | ||
185 | |||
186 | return true; | ||
187 | } | ||
188 | |||
189 | TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr) | ||
190 | { | ||
191 | LOG_INFO() << "Voicing" << text << "->" << wavfile; | ||
192 | |||
193 | QString path = RbSettings::subValue("festival-client", | ||
194 | RbSettings::TtsPath).toString(); | ||
195 | QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp" | ||
196 | " --output \"%2\" --prolog \"%3\" - ").arg(path).arg(wavfile).arg(prologPath); | ||
197 | LOG_INFO() << "Client cmd:" << cmd; | ||
198 | |||
199 | QProcess clientProcess; | ||
200 | clientProcess.start(cmd); | ||
201 | clientProcess.write(QString("%1.\n").arg(text).toLatin1()); | ||
202 | clientProcess.waitForBytesWritten(); | ||
203 | clientProcess.closeWriteChannel(); | ||
204 | clientProcess.waitForReadyRead(); | ||
205 | QString response = clientProcess.readAll(); | ||
206 | response = response.trimmed(); | ||
207 | if(!response.contains("Utterance")) | ||
208 | { | ||
209 | LOG_WARNING() << "Could not voice string: " << response; | ||
210 | *errStr = tr("engine could not voice string"); | ||
211 | return Warning; | ||
212 | /* do not stop the voicing process because of a single string | ||
213 | TODO: needs proper settings */ | ||
214 | } | ||
215 | clientProcess.closeReadChannel(QProcess::StandardError); | ||
216 | clientProcess.closeReadChannel(QProcess::StandardOutput); | ||
217 | clientProcess.terminate(); | ||
218 | clientProcess.kill(); | ||
219 | |||
220 | return NoError; | ||
221 | } | ||
222 | |||
223 | bool TTSFestival::configOk() | ||
224 | { | ||
225 | bool ret; | ||
226 | if (currentPath == "") | ||
227 | { | ||
228 | QString serverPath = RbSettings::subValue("festival-server", | ||
229 | RbSettings::TtsPath).toString(); | ||
230 | QString clientPath = RbSettings::subValue("festival-client", | ||
231 | RbSettings::TtsPath).toString(); | ||
232 | |||
233 | ret = QFileInfo(serverPath).isExecutable() && | ||
234 | QFileInfo(clientPath).isExecutable(); | ||
235 | if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0 | ||
236 | && voices.size() > 0) | ||
237 | ret = ret && (voices.indexOf(RbSettings::subValue("festival", | ||
238 | RbSettings::TtsVoice).toString()) != -1); | ||
239 | } | ||
240 | else /* If we're currently configuring the server, we need to know that | ||
241 | the entered path is valid */ | ||
242 | ret = QFileInfo(currentPath).isExecutable(); | ||
243 | |||
244 | return ret; | ||
245 | } | ||
246 | |||
247 | QStringList TTSFestival::getVoiceList() | ||
248 | { | ||
249 | if(!configOk()) | ||
250 | return QStringList(); | ||
251 | |||
252 | if(voices.size() > 0) | ||
253 | { | ||
254 | LOG_INFO() << "Using voice cache"; | ||
255 | return voices; | ||
256 | } | ||
257 | |||
258 | QString response = queryServer("(voice.list)", 10000); | ||
259 | |||
260 | // get the 2nd line. It should be (<voice_name>, <voice_name>) | ||
261 | response = response.mid(response.indexOf('\n') + 1, -1); | ||
262 | response = response.left(response.indexOf('\n')).trimmed(); | ||
263 | |||
264 | voices = response.mid(1, response.size()-2).split(' '); | ||
265 | |||
266 | voices.sort(); | ||
267 | if (voices.size() == 1 && voices[0].size() == 0) | ||
268 | voices.removeAt(0); | ||
269 | if (voices.size() > 0) | ||
270 | LOG_INFO() << "Voices:" << voices; | ||
271 | else | ||
272 | LOG_WARNING() << "No voices. Response was:" << response; | ||
273 | |||
274 | return voices; | ||
275 | } | ||
276 | |||
277 | QString TTSFestival::getVoiceInfo(QString voice) | ||
278 | { | ||
279 | if(!configOk()) | ||
280 | return ""; | ||
281 | |||
282 | if(!getVoiceList().contains(voice)) | ||
283 | return ""; | ||
284 | |||
285 | if(voiceDescriptions.contains(voice)) | ||
286 | return voiceDescriptions[voice]; | ||
287 | |||
288 | QString response = queryServer(QString("(voice.description '%1)").arg(voice), | ||
289 | 10000); | ||
290 | |||
291 | if (response == "") | ||
292 | { | ||
293 | voiceDescriptions[voice]=tr("No description available"); | ||
294 | } | ||
295 | else | ||
296 | { | ||
297 | response = response.remove(QRegExp("(description \"*\")", | ||
298 | Qt::CaseInsensitive, QRegExp::Wildcard)); | ||
299 | LOG_INFO() << "voiceInfo w/o descr:" << response; | ||
300 | response = response.remove(')'); | ||
301 | #if QT_VERSION >= 0x050e00 | ||
302 | QStringList responseLines = response.split('(', Qt::SkipEmptyParts); | ||
303 | #else | ||
304 | QStringList responseLines = response.split('(', QString::SkipEmptyParts); | ||
305 | #endif | ||
306 | responseLines.removeAt(0); // the voice name itself | ||
307 | |||
308 | QString description; | ||
309 | foreach(QString line, responseLines) | ||
310 | { | ||
311 | line = line.remove('('); | ||
312 | line = line.simplified(); | ||
313 | |||
314 | line[0] = line[0].toUpper(); // capitalize the key | ||
315 | |||
316 | int firstSpace = line.indexOf(' '); | ||
317 | if (firstSpace > 0) | ||
318 | { | ||
319 | // add a colon between the key and the value | ||
320 | line = line.insert(firstSpace, ':'); | ||
321 | // capitalize the value | ||
322 | line[firstSpace+2] = line[firstSpace+2].toUpper(); | ||
323 | } | ||
324 | |||
325 | description += line + "\n"; | ||
326 | } | ||
327 | voiceDescriptions[voice] = description.trimmed(); | ||
328 | } | ||
329 | |||
330 | return voiceDescriptions[voice]; | ||
331 | } | ||
332 | |||
333 | QString TTSFestival::queryServer(QString query, int timeout) | ||
334 | { | ||
335 | if(!configOk()) | ||
336 | return ""; | ||
337 | |||
338 | // this operation could take some time | ||
339 | emit busy(); | ||
340 | |||
341 | LOG_INFO() << "queryServer with" << query; | ||
342 | |||
343 | if (!ensureServerRunning()) | ||
344 | { | ||
345 | LOG_ERROR() << "queryServer: ensureServerRunning failed"; | ||
346 | emit busyEnd(); | ||
347 | return ""; | ||
348 | } | ||
349 | |||
350 | QString response; | ||
351 | |||
352 | QDateTime endTime; | ||
353 | if(timeout > 0) | ||
354 | endTime = QDateTime::currentDateTime().addMSecs(timeout); | ||
355 | |||
356 | /* Festival is *extremely* unreliable. Although at this | ||
357 | * point we are sure that SIOD is accepting commands, | ||
358 | * we might end up with an empty response. Hence, the loop. | ||
359 | */ | ||
360 | while(true) | ||
361 | { | ||
362 | QCoreApplication::processEvents(QEventLoop::AllEvents, 50); | ||
363 | QTcpSocket socket; | ||
364 | |||
365 | socket.connectToHost("localhost", 1314); | ||
366 | socket.waitForConnected(); | ||
367 | |||
368 | if(socket.state() == QAbstractSocket::ConnectedState) | ||
369 | { | ||
370 | socket.write(QString("%1\n").arg(query).toLatin1()); | ||
371 | socket.waitForBytesWritten(); | ||
372 | socket.waitForReadyRead(); | ||
373 | |||
374 | response = socket.readAll().trimmed(); | ||
375 | |||
376 | if (response != "LP" && response != "") | ||
377 | break; | ||
378 | } | ||
379 | socket.abort(); | ||
380 | socket.disconnectFromHost(); | ||
381 | |||
382 | if(timeout > 0 && QDateTime::currentDateTime() >= endTime) | ||
383 | { | ||
384 | emit busyEnd(); | ||
385 | return ""; | ||
386 | } | ||
387 | /* make sure we wait a little as we don't want to flood the server | ||
388 | * with requests */ | ||
389 | QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500); | ||
390 | while(QDateTime::currentDateTime() < tmpEndTime) | ||
391 | QCoreApplication::processEvents(QEventLoop::AllEvents); | ||
392 | } | ||
393 | if(response == "nil") | ||
394 | { | ||
395 | emit busyEnd(); | ||
396 | return ""; | ||
397 | } | ||
398 | |||
399 | QStringList lines = response.split('\n'); | ||
400 | if(lines.size() > 2) | ||
401 | { | ||
402 | lines.removeFirst(); /* should be LP */ | ||
403 | lines.removeLast(); /* should be ft_StUfF_keyOK */ | ||
404 | } | ||
405 | else | ||
406 | LOG_ERROR() << "Response too short:" << response; | ||
407 | |||
408 | emit busyEnd(); | ||
409 | return lines.join("\n"); | ||
410 | |||
411 | } | ||
412 | |||