62108a9613
Get rid of some unnecessary object creating / copying by using references. Change-Id: Ia44e34f6f66d230caa9af7ef7c0eca73be12de2a
420 lines
13 KiB
C++
420 lines
13 KiB
C++
/***************************************************************************
|
|
* __________ __ ___.
|
|
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
|
|
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
|
|
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
|
|
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|
* \/ \/ \/ \/ \/
|
|
*
|
|
* Copyright (C) 2007 by Dominik Wenger
|
|
*
|
|
* All files in this archive are subject to the GNU General Public License.
|
|
* See the file COPYING in the source tree root for full license agreement.
|
|
*
|
|
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
* KIND, either express or implied.
|
|
*
|
|
****************************************************************************/
|
|
|
|
#include <QtCore>
|
|
#include <QTcpSocket>
|
|
|
|
#include "ttsfestival.h"
|
|
#include "utils.h"
|
|
#include "rbsettings.h"
|
|
#include "Logger.h"
|
|
|
|
TTSFestival::~TTSFestival()
|
|
{
|
|
LOG_INFO() << "Destroying instance";
|
|
stop();
|
|
}
|
|
|
|
TTSBase::Capabilities TTSFestival::capabilities()
|
|
{
|
|
return RunInParallel;
|
|
}
|
|
|
|
void TTSFestival::generateSettings()
|
|
{
|
|
// server path
|
|
QString exepath = RbSettings::subValue("festival-server",
|
|
RbSettings::TtsPath).toString();
|
|
if(exepath == "" ) exepath = Utils::findExecutable("festival");
|
|
insertSetting(eSERVERPATH,new EncTtsSetting(this,
|
|
EncTtsSetting::eSTRING, "Path to Festival server:",
|
|
exepath,EncTtsSetting::eBROWSEBTN));
|
|
|
|
// client path
|
|
QString clientpath = RbSettings::subValue("festival-client",
|
|
RbSettings::TtsPath).toString();
|
|
if(clientpath == "" ) clientpath = Utils::findExecutable("festival_client");
|
|
insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,
|
|
tr("Path to Festival client:"),
|
|
clientpath,EncTtsSetting::eBROWSEBTN));
|
|
|
|
// voice
|
|
EncTtsSetting* setting = new EncTtsSetting(this,
|
|
EncTtsSetting::eSTRINGLIST, tr("Voice:"),
|
|
RbSettings::subValue("festival", RbSettings::TtsVoice),
|
|
getVoiceList(), EncTtsSetting::eREFRESHBTN);
|
|
connect(setting, &EncTtsSetting::refresh,
|
|
this, &TTSFestival::updateVoiceList);
|
|
connect(setting, &EncTtsSetting::dataChanged,
|
|
this, &TTSFestival::clearVoiceDescription);
|
|
insertSetting(eVOICE,setting);
|
|
|
|
//voice description
|
|
setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,
|
|
tr("Voice description:"),"",EncTtsSetting::eREFRESHBTN);
|
|
connect(setting, &EncTtsSetting::refresh,
|
|
this, &TTSFestival::updateVoiceDescription);
|
|
insertSetting(eVOICEDESC,setting);
|
|
}
|
|
|
|
void TTSFestival::saveSettings()
|
|
{
|
|
//save settings in user config
|
|
RbSettings::setSubValue("festival-server",
|
|
RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString());
|
|
RbSettings::setSubValue("festival-client",
|
|
RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString());
|
|
RbSettings::setSubValue("festival",
|
|
RbSettings::TtsVoice,getSetting(eVOICE)->current().toString());
|
|
|
|
RbSettings::sync();
|
|
}
|
|
|
|
void TTSFestival::updateVoiceDescription()
|
|
{
|
|
// get voice Info with current voice and path
|
|
currentPath = getSetting(eSERVERPATH)->current().toString();
|
|
QString info = getVoiceInfo(getSetting(eVOICE)->current().toString());
|
|
currentPath = "";
|
|
|
|
getSetting(eVOICEDESC)->setCurrent(info);
|
|
}
|
|
|
|
void TTSFestival::clearVoiceDescription()
|
|
{
|
|
getSetting(eVOICEDESC)->setCurrent("");
|
|
}
|
|
|
|
void TTSFestival::updateVoiceList()
|
|
{
|
|
currentPath = getSetting(eSERVERPATH)->current().toString();
|
|
QStringList voiceList = getVoiceList();
|
|
currentPath = "";
|
|
|
|
getSetting(eVOICE)->setList(voiceList);
|
|
if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0));
|
|
else getSetting(eVOICE)->setCurrent("");
|
|
}
|
|
|
|
void TTSFestival::startServer()
|
|
{
|
|
if(!configOk())
|
|
return;
|
|
|
|
if(serverProcess.state() != QProcess::Running)
|
|
{
|
|
QString path;
|
|
/* currentPath is set by the GUI - if it's set, it is the currently set
|
|
path in the configuration GUI; if it's not set, use the saved path */
|
|
if (currentPath.isEmpty())
|
|
path = RbSettings::subValue("festival-server",RbSettings::TtsPath).toString();
|
|
else
|
|
path = currentPath;
|
|
|
|
serverProcess.start(path, QStringList("--server"));
|
|
serverProcess.waitForStarted();
|
|
|
|
if(serverProcess.state() == QProcess::Running)
|
|
LOG_INFO() << "Server is up and running";
|
|
else
|
|
LOG_ERROR() << "Server failed to start, state:"
|
|
<< serverProcess.state();
|
|
}
|
|
}
|
|
|
|
bool TTSFestival::ensureServerRunning()
|
|
{
|
|
if(serverProcess.state() != QProcess::Running)
|
|
{
|
|
startServer();
|
|
}
|
|
return serverProcess.state() == QProcess::Running;
|
|
}
|
|
|
|
bool TTSFestival::start(QString* errStr)
|
|
{
|
|
LOG_INFO() << "Starting server with voice"
|
|
<< RbSettings::subValue("festival", RbSettings::TtsVoice).toString();
|
|
|
|
bool running = ensureServerRunning();
|
|
if (!RbSettings::subValue("festival",RbSettings::TtsVoice).toString().isEmpty())
|
|
{
|
|
/* There's no harm in using both methods to set the voice .. */
|
|
QString voiceSelect = QString("(voice.select '%1)\n")
|
|
.arg(RbSettings::subValue("festival", RbSettings::TtsVoice).toString());
|
|
queryServer(voiceSelect, 3000);
|
|
|
|
if(prologFile.open())
|
|
{
|
|
prologFile.write(voiceSelect.toLatin1());
|
|
prologFile.close();
|
|
prologPath = QFileInfo(prologFile).absoluteFilePath();
|
|
LOG_INFO() << "Prolog created at" << prologPath;
|
|
}
|
|
|
|
}
|
|
|
|
if (!running)
|
|
(*errStr) = tr("Festival could not be started");
|
|
return running;
|
|
}
|
|
|
|
bool TTSFestival::stop()
|
|
{
|
|
serverProcess.terminate();
|
|
serverProcess.kill();
|
|
|
|
return true;
|
|
}
|
|
|
|
TTSStatus TTSFestival::voice(const QString& text, const QString& wavfile, QString* errStr)
|
|
{
|
|
LOG_INFO() << "Voicing" << text << "->" << wavfile;
|
|
|
|
QString path = RbSettings::subValue("festival-client",
|
|
RbSettings::TtsPath).toString();
|
|
QStringList cmd;
|
|
cmd << "--server" << "localhost" << "--otype" << "riff" << "--ttw"
|
|
<< "--withlisp" << "--output" << wavfile << "--prolog" << prologPath << "-";
|
|
LOG_INFO() << "Client cmd:" << path << cmd;
|
|
|
|
QProcess clientProcess;
|
|
clientProcess.start(path, cmd);
|
|
clientProcess.write(QString("%1.\n").arg(text).toLatin1());
|
|
clientProcess.waitForBytesWritten();
|
|
clientProcess.closeWriteChannel();
|
|
clientProcess.waitForReadyRead();
|
|
QString response = clientProcess.readAll();
|
|
response = response.trimmed();
|
|
if(!response.contains("Utterance"))
|
|
{
|
|
LOG_WARNING() << "Could not voice string: " << response;
|
|
*errStr = tr("engine could not voice string");
|
|
return Warning;
|
|
/* do not stop the voicing process because of a single string
|
|
TODO: needs proper settings */
|
|
}
|
|
clientProcess.closeReadChannel(QProcess::StandardError);
|
|
clientProcess.closeReadChannel(QProcess::StandardOutput);
|
|
clientProcess.terminate();
|
|
clientProcess.kill();
|
|
|
|
return NoError;
|
|
}
|
|
|
|
bool TTSFestival::configOk()
|
|
{
|
|
bool ret;
|
|
if (currentPath == "")
|
|
{
|
|
QString serverPath = RbSettings::subValue("festival-server",
|
|
RbSettings::TtsPath).toString();
|
|
QString clientPath = RbSettings::subValue("festival-client",
|
|
RbSettings::TtsPath).toString();
|
|
|
|
ret = QFileInfo(serverPath).isExecutable() &&
|
|
QFileInfo(clientPath).isExecutable();
|
|
if(RbSettings::subValue("festival",RbSettings::TtsVoice).toString().size() > 0
|
|
&& voices.size() > 0)
|
|
ret = ret && (voices.indexOf(RbSettings::subValue("festival",
|
|
RbSettings::TtsVoice).toString()) != -1);
|
|
}
|
|
else /* If we're currently configuring the server, we need to know that
|
|
the entered path is valid */
|
|
ret = QFileInfo(currentPath).isExecutable();
|
|
|
|
return ret;
|
|
}
|
|
|
|
QStringList TTSFestival::getVoiceList()
|
|
{
|
|
if(!configOk())
|
|
return QStringList();
|
|
|
|
if(voices.size() > 0)
|
|
{
|
|
LOG_INFO() << "Using voice cache";
|
|
return voices;
|
|
}
|
|
|
|
QString response = queryServer("(voice.list)", 10000);
|
|
|
|
// get the 2nd line. It should be (<voice_name>, <voice_name>)
|
|
response = response.mid(response.indexOf('\n') + 1, -1);
|
|
response = response.left(response.indexOf('\n')).trimmed();
|
|
|
|
voices = response.mid(1, response.size()-2).split(' ');
|
|
|
|
voices.sort();
|
|
if (voices.size() == 1 && voices[0].size() == 0)
|
|
voices.removeAt(0);
|
|
if (voices.size() > 0)
|
|
LOG_INFO() << "Voices:" << voices;
|
|
else
|
|
LOG_WARNING() << "No voices. Response was:" << response;
|
|
|
|
return voices;
|
|
}
|
|
|
|
QString TTSFestival::getVoiceInfo(QString voice)
|
|
{
|
|
if(!configOk())
|
|
return "";
|
|
|
|
if(!getVoiceList().contains(voice))
|
|
return "";
|
|
|
|
if(voiceDescriptions.contains(voice))
|
|
return voiceDescriptions[voice];
|
|
|
|
QString response = queryServer(QString("(voice.description '%1)").arg(voice),
|
|
10000);
|
|
|
|
if (response == "")
|
|
{
|
|
voiceDescriptions[voice]=tr("No description available");
|
|
}
|
|
else
|
|
{
|
|
response = response.remove(QRegularExpression("(description \".*\")",
|
|
QRegularExpression::CaseInsensitiveOption));
|
|
LOG_INFO() << "voiceInfo w/o descr:" << response;
|
|
response = response.remove(')');
|
|
#if QT_VERSION >= 0x050e00
|
|
QStringList responseLines = response.split('(', Qt::SkipEmptyParts);
|
|
#else
|
|
QStringList responseLines = response.split('(', QString::SkipEmptyParts);
|
|
#endif
|
|
responseLines.removeAt(0); // the voice name itself
|
|
|
|
QString description;
|
|
foreach(QString line, responseLines)
|
|
{
|
|
line = line.remove('(');
|
|
line = line.simplified();
|
|
|
|
line[0] = line[0].toUpper(); // capitalize the key
|
|
|
|
int firstSpace = line.indexOf(' ');
|
|
if (firstSpace > 0)
|
|
{
|
|
// add a colon between the key and the value
|
|
line = line.insert(firstSpace, ':');
|
|
// capitalize the value
|
|
line[firstSpace+2] = line[firstSpace+2].toUpper();
|
|
}
|
|
|
|
description += line + "\n";
|
|
}
|
|
voiceDescriptions[voice] = description.trimmed();
|
|
}
|
|
|
|
return voiceDescriptions[voice];
|
|
}
|
|
|
|
QString TTSFestival::queryServer(QString query, int timeout)
|
|
{
|
|
// make sure we always abort at some point.
|
|
if(timeout == 0)
|
|
timeout = 60000;
|
|
if(!configOk())
|
|
return "";
|
|
|
|
// this operation could take some time
|
|
emit busy(true);
|
|
|
|
LOG_INFO() << "queryServer with" << query;
|
|
|
|
if (!ensureServerRunning())
|
|
{
|
|
LOG_ERROR() << "queryServer: ensureServerRunning failed";
|
|
emit busy(false);
|
|
return "";
|
|
}
|
|
|
|
|
|
QDateTime endTime = QDateTime::currentDateTime().addMSecs(timeout);
|
|
|
|
/* Festival is *extremely* unreliable. Although at this
|
|
* point we are sure that SIOD is accepting commands,
|
|
* we might end up with an empty response. Hence, the loop.
|
|
*/
|
|
QTcpSocket socket;
|
|
QString response;
|
|
while(QDateTime::currentDateTime() < endTime)
|
|
{
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
|
|
|
|
if(socket.state() != QAbstractSocket::ConnectedState)
|
|
{
|
|
LOG_INFO() << "socket not (yet) connected, trying again.";
|
|
socket.connectToHost("localhost", 1314);
|
|
// appears we need to recheck the state still.
|
|
socket.waitForConnected();
|
|
}
|
|
else
|
|
{
|
|
// seems to be necessary to resend the request at times.
|
|
socket.write(QString("%1\n").arg(query).toLatin1());
|
|
socket.waitForBytesWritten();
|
|
socket.waitForReadyRead();
|
|
|
|
// we might not get the complete response on the first read.
|
|
// Concatenate until we got a full response.
|
|
response += socket.readAll();
|
|
|
|
// The query response ends with this.
|
|
if (response.contains("ft_StUfF_keyOK"))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* make sure we wait a little as we don't want to flood the server
|
|
* with requests */
|
|
QDateTime tmpEndTime = QDateTime::currentDateTime().addMSecs(500);
|
|
while(QDateTime::currentDateTime() < tmpEndTime)
|
|
QCoreApplication::processEvents(QEventLoop::AllEvents);
|
|
}
|
|
emit busy(false);
|
|
socket.disconnectFromHost();
|
|
|
|
if(response == "nil")
|
|
{
|
|
return "";
|
|
}
|
|
|
|
/* The response starts with "LP\n", and ends with "ft_StUfF_keyOK", but we
|
|
* could get trailing data -- we might have sent the request more than
|
|
* once. Use a regex to get the actual response part.
|
|
*/
|
|
QRegularExpression regex("LP\\n(.*?)\\nft_StUfF_keyOK",
|
|
QRegularExpression::MultilineOption
|
|
| QRegularExpression::DotMatchesEverythingOption);
|
|
QRegularExpressionMatch match = regex.match(response);
|
|
if(match.hasMatch())
|
|
{
|
|
response = match.captured(1);
|
|
}
|
|
else {
|
|
LOG_WARNING() << "Invalid Festival response." << response;
|
|
}
|
|
|
|
return response.trimmed();
|
|
}
|
|
|