diff options
Diffstat (limited to 'rbutil/rbutilqt/base/tts.cpp')
-rw-r--r-- | rbutil/rbutilqt/base/tts.cpp | 656 |
1 files changed, 656 insertions, 0 deletions
diff --git a/rbutil/rbutilqt/base/tts.cpp b/rbutil/rbutilqt/base/tts.cpp new file mode 100644 index 0000000000..d55ba9e739 --- /dev/null +++ b/rbutil/rbutilqt/base/tts.cpp @@ -0,0 +1,656 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * + * Copyright (C) 2007 by Dominik Wenger + * $Id$ + * + * 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 "tts.h" +#include "utils.h" +/********************************************************************* +* TTS Base +**********************************************************************/ +QMap<QString,QString> TTSBase::ttsList; + +TTSBase::TTSBase(QObject* parent): EncTtsSettingInterface(parent) +{ + +} + +// static functions +void TTSBase::initTTSList() +{ + ttsList["espeak"] = "Espeak TTS Engine"; + ttsList["flite"] = "Flite TTS Engine"; + ttsList["swift"] = "Swift TTS Engine"; +#if defined(Q_OS_WIN) + ttsList["sapi"] = "Sapi TTS Engine"; +#endif +#if defined(Q_OS_LINUX) + ttsList["festival"] = "Festival TTS Engine"; +#endif +} + +// function to get a specific encoder +TTSBase* TTSBase::getTTS(QObject* parent,QString ttsName) +{ + + TTSBase* tts; +#if defined(Q_OS_WIN) + if(ttsName == "sapi") + { + tts = new TTSSapi(parent); + return tts; + } + else +#endif +#if defined(Q_OS_LINUX) + if (ttsName == "festival") + { + tts = new TTSFestival(parent); + return tts; + } + else +#endif + if (true) // fix for OS other than WIN or LINUX + { + tts = new TTSExes(ttsName,parent); + return tts; + } +} + +// get the list of encoders, nice names +QStringList TTSBase::getTTSList() +{ + // init list if its empty + if(ttsList.count() == 0) + initTTSList(); + + return ttsList.keys(); +} + +// get nice name of a specific tts +QString TTSBase::getTTSName(QString tts) +{ + if(ttsList.isEmpty()) + initTTSList(); + return ttsList.value(tts); +} + + +/********************************************************************* +* General TTS Exes +**********************************************************************/ +TTSExes::TTSExes(QString name,QObject* parent) : TTSBase(parent) +{ + m_name = name; + + m_TemplateMap["espeak"] = "\"%exe\" %options -w \"%wavfile\" \"%text\""; + m_TemplateMap["flite"] = "\"%exe\" %options -o \"%wavfile\" -t \"%text\""; + m_TemplateMap["swift"] = "\"%exe\" %options -o \"%wavfile\" \"%text\""; + +} + +void TTSExes::generateSettings() +{ + QString exepath =settings->subValue(m_name,RbSettings::TtsPath).toString(); + if(exepath == "") exepath = findExecutable(m_name); + + insertSetting(eEXEPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to TTS engine:",exepath,EncTtsSetting::eBROWSEBTN)); + insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,"TTS enginge options:",settings->subValue(m_name,RbSettings::TtsOptions))); +} + +void TTSExes::saveSettings() +{ + settings->setSubValue(m_name,RbSettings::TtsPath,getSetting(eEXEPATH)->current().toString()); + settings->setSubValue(m_name,RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString()); + settings->sync(); +} + +bool TTSExes::start(QString *errStr) +{ + m_TTSexec = settings->subValue(m_name,RbSettings::TtsPath).toString(); + m_TTSOpts = settings->subValue(m_name,RbSettings::TtsOptions).toString(); + + m_TTSTemplate = m_TemplateMap.value(m_name); + + QFileInfo tts(m_TTSexec); + if(tts.exists()) + { + return true; + } + else + { + *errStr = tr("TTS executable not found"); + return false; + } +} + +TTSStatus TTSExes::voice(QString text,QString wavfile, QString *errStr) +{ + (void) errStr; + QString execstring = m_TTSTemplate; + + execstring.replace("%exe",m_TTSexec); + execstring.replace("%options",m_TTSOpts); + execstring.replace("%wavfile",wavfile); + execstring.replace("%text",text); + //qDebug() << "voicing" << execstring; + QProcess::execute(execstring); + return NoError; + +} + +bool TTSExes::configOk() +{ + QString path = settings->subValue(m_name,RbSettings::TtsPath).toString(); + + if (QFileInfo(path).exists()) + return true; + + return false; +} + +/********************************************************************* +* TTS Sapi +**********************************************************************/ +TTSSapi::TTSSapi(QObject* parent) : TTSBase(parent) +{ + m_TTSTemplate = "cscript //nologo \"%exe\" /language:%lang /voice:\"%voice\" /speed:%speed \"%options\""; + defaultLanguage ="english"; + m_sapi4 =false; +} + +void TTSSapi::generateSettings() +{ + // language + QStringList languages = settings->languages(); + languages.sort(); + EncTtsSetting* setting =new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,"Language:",settings->subValue("sapi",RbSettings::TtsLanguage),languages); + connect(setting,SIGNAL(dataChanged()),this,SLOT(updateVoiceList())); + insertSetting(eLANGUAGE,setting); + // voice + setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,"Voice:",settings->subValue("sapi",RbSettings::TtsVoice),getVoiceList(settings->subValue("sapi",RbSettings::TtsLanguage).toString()),EncTtsSetting::eREFRESHBTN); + connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList())); + insertSetting(eVOICE,setting); + //speed + insertSetting(eSPEED,new EncTtsSetting(this,EncTtsSetting::eINT,"Speed:",settings->subValue("sapi",RbSettings::TtsSpeed),-10,10)); + // options + insertSetting(eOPTIONS,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Options:",settings->subValue("sapi",RbSettings::TtsOptions))); + +} + +void TTSSapi::saveSettings() +{ + //save settings in user config + settings->setSubValue("sapi",RbSettings::TtsLanguage,getSetting(eLANGUAGE)->current().toString()); + settings->setSubValue("sapi",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); + settings->setSubValue("sapi",RbSettings::TtsSpeed,getSetting(eSPEED)->current().toInt()); + settings->setSubValue("sapi",RbSettings::TtsOptions,getSetting(eOPTIONS)->current().toString()); + + settings->sync(); +} + +void TTSSapi::updateVoiceList() +{ + qDebug() << "update voiceList"; + QStringList voiceList = getVoiceList(getSetting(eLANGUAGE)->current().toString()); + getSetting(eVOICE)->setList(voiceList); + if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); + else getSetting(eVOICE)->setCurrent(""); +} + +bool TTSSapi::start(QString *errStr) +{ + + m_TTSOpts = settings->subValue("sapi",RbSettings::TtsOptions).toString(); + m_TTSLanguage =settings->subValue("sapi",RbSettings::TtsLanguage).toString(); + m_TTSVoice=settings->subValue("sapi",RbSettings::TtsVoice).toString(); + m_TTSSpeed=settings->subValue("sapi",RbSettings::TtsSpeed).toString(); + m_sapi4 = settings->subValue("sapi",RbSettings::TtsUseSapi4).toBool(); + + QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); + QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs"); + m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs"; + + QFileInfo tts(m_TTSexec); + if(!tts.exists()) + { + *errStr = tr("Could not copy the Sapi-script"); + return false; + } + // create the voice process + QString execstring = m_TTSTemplate; + execstring.replace("%exe",m_TTSexec); + execstring.replace("%options",m_TTSOpts); + execstring.replace("%lang",m_TTSLanguage); + execstring.replace("%voice",m_TTSVoice); + execstring.replace("%speed",m_TTSSpeed); + + if(m_sapi4) + execstring.append(" /sapi4 "); + + qDebug() << "init" << execstring; + voicescript = new QProcess(NULL); + //connect(voicescript,SIGNAL(readyReadStandardError()),this,SLOT(error())); + + voicescript->start(execstring); + if(!voicescript->waitForStarted()) + { + *errStr = tr("Could not start the Sapi-script"); + return false; + } + + if(!voicescript->waitForReadyRead(300)) + { + *errStr = voicescript->readAllStandardError(); + if(*errStr != "") + return false; + } + + voicestream = new QTextStream(voicescript); + voicestream->setCodec("UTF16-LE"); + + return true; +} + + +QStringList TTSSapi::getVoiceList(QString language) +{ + QStringList result; + + QFile::copy(":/builtin/sapi_voice.vbs",QDir::tempPath() + "/sapi_voice.vbs"); + m_TTSexec = QDir::tempPath() +"/sapi_voice.vbs"; + + QFileInfo tts(m_TTSexec); + if(!tts.exists()) + return result; + + // create the voice process + QString execstring = "cscript //nologo \"%exe\" /language:%lang /listvoices"; + execstring.replace("%exe",m_TTSexec); + execstring.replace("%lang",language); + + if(settings->value(RbSettings::TtsUseSapi4).toBool()) + execstring.append(" /sapi4 "); + + qDebug() << "init" << execstring; + voicescript = new QProcess(NULL); + voicescript->start(execstring); + qDebug() << "wait for started"; + if(!voicescript->waitForStarted()) + return result; + voicescript->closeWriteChannel(); + voicescript->waitForReadyRead(); + + QString dataRaw = voicescript->readAllStandardError().data(); + result = dataRaw.split(",",QString::SkipEmptyParts); + if(result.size() > 0) + { + result.sort(); + result.removeFirst(); + for(int i = 0; i< result.size();i++) + { + result[i] = result.at(i).simplified(); + } + } + + delete voicescript; + QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner + |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser + |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup + |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther ); + QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); + return result; +} + + + +TTSStatus TTSSapi::voice(QString text,QString wavfile, QString *errStr) +{ + (void) errStr; + QString query = "SPEAK\t"+wavfile+"\t"+text+"\r\n"; + qDebug() << "voicing" << query; + *voicestream << query; + *voicestream << "SYNC\tbla\r\n"; + voicestream->flush(); + voicescript->waitForReadyRead(); + return NoError; +} + +bool TTSSapi::stop() +{ + + *voicestream << "QUIT\r\n"; + voicestream->flush(); + voicescript->waitForFinished(); + delete voicestream; + delete voicescript; + QFile::setPermissions(QDir::tempPath() +"/sapi_voice.vbs",QFile::ReadOwner |QFile::WriteOwner|QFile::ExeOwner + |QFile::ReadUser| QFile::WriteUser| QFile::ExeUser + |QFile::ReadGroup |QFile::WriteGroup |QFile::ExeGroup + |QFile::ReadOther |QFile::WriteOther |QFile::ExeOther ); + QFile::remove(QDir::tempPath() +"/sapi_voice.vbs"); + return true; +} + +bool TTSSapi::configOk() +{ + if(settings->subValue("sapi",RbSettings::TtsVoice).toString().isEmpty()) + return false; + return true; +} +/********************************************************************** + * TSSFestival - client-server wrapper + **********************************************************************/ +TTSFestival::~TTSFestival() +{ + stop(); +} + +void TTSFestival::generateSettings() +{ + // server path + QString exepath = settings->subValue("festival-server",RbSettings::TtsPath).toString(); + if(exepath == "" ) exepath = findExecutable("festival"); + insertSetting(eSERVERPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival server:",exepath,EncTtsSetting::eBROWSEBTN)); + + // client path + QString clientpath = settings->subValue("festival-client",RbSettings::TtsPath).toString(); + if(clientpath == "" ) clientpath = findExecutable("festival_client"); + insertSetting(eCLIENTPATH,new EncTtsSetting(this,EncTtsSetting::eSTRING,"Path to Festival client:",clientpath,EncTtsSetting::eBROWSEBTN)); + + // voice + EncTtsSetting* setting = new EncTtsSetting(this,EncTtsSetting::eSTRINGLIST,"Voice:",settings->subValue("festival",RbSettings::TtsVoice),getVoiceList(exepath),EncTtsSetting::eREFRESHBTN); + connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceList())); + connect(setting,SIGNAL(dataChanged()),this,SLOT(clearVoiceDescription())); + insertSetting(eVOICE,setting); + + //voice description + setting = new EncTtsSetting(this,EncTtsSetting::eREADONLYSTRING,"Voice description:","",EncTtsSetting::eREFRESHBTN); + connect(setting,SIGNAL(refresh()),this,SLOT(updateVoiceDescription())); + insertSetting(eVOICEDESC,setting); +} + +void TTSFestival::saveSettings() +{ + //save settings in user config + settings->setSubValue("festival-server",RbSettings::TtsPath,getSetting(eSERVERPATH)->current().toString()); + settings->setSubValue("festival-client",RbSettings::TtsPath,getSetting(eCLIENTPATH)->current().toString()); + settings->setSubValue("festival",RbSettings::TtsVoice,getSetting(eVOICE)->current().toString()); + + settings->sync(); +} + +void TTSFestival::updateVoiceDescription() +{ + // get voice Info with current voice and path + QString info = getVoiceInfo(getSetting(eVOICE)->current().toString(),getSetting(eSERVERPATH)->current().toString()); + getSetting(eVOICEDESC)->setCurrent(info); +} + +void TTSFestival::clearVoiceDescription() +{ + getSetting(eVOICEDESC)->setCurrent(""); +} + +void TTSFestival::updateVoiceList() +{ + QStringList voiceList = getVoiceList(getSetting(eSERVERPATH)->current().toString()); + getSetting(eVOICE)->setList(voiceList); + if(voiceList.size() > 0) getSetting(eVOICE)->setCurrent(voiceList.at(0)); + else getSetting(eVOICE)->setCurrent(""); +} + +void TTSFestival::startServer(QString path) +{ + if(!configOk()) + return; + + if(path == "") + path = settings->subValue("festival-server",RbSettings::TtsPath).toString(); + + serverProcess.start(QString("%1 --server").arg(path)); + serverProcess.waitForStarted(); + + queryServer("(getpid)",300,path); + if(serverProcess.state() == QProcess::Running) + qDebug() << "Festival is up and running"; + else + qDebug() << "Festival failed to start"; +} + +void TTSFestival::ensureServerRunning(QString path) +{ + if(serverProcess.state() != QProcess::Running) + { + startServer(path); + } +} + +bool TTSFestival::start(QString* errStr) +{ + (void) errStr; + ensureServerRunning(); + if (!settings->subValue("festival",RbSettings::TtsVoice).toString().isEmpty()) + queryServer(QString("(voice.select '%1)") + .arg(settings->subValue("festival", RbSettings::TtsVoice).toString())); + + return true; +} + +bool TTSFestival::stop() +{ + serverProcess.terminate(); + serverProcess.kill(); + + return true; +} + +TTSStatus TTSFestival::voice(QString text, QString wavfile, QString* errStr) +{ + qDebug() << text << "->" << wavfile; + + QString path = settings->subValue("festival-client",RbSettings::TtsPath).toString(); + QString cmd = QString("%1 --server localhost --otype riff --ttw --withlisp --output \"%2\" - ").arg(path).arg(wavfile); + qDebug() << cmd; + + QProcess clientProcess; + clientProcess.start(cmd); + clientProcess.write(QString("%1.\n").arg(text).toAscii()); + clientProcess.waitForBytesWritten(); + clientProcess.closeWriteChannel(); + clientProcess.waitForReadyRead(); + QString response = clientProcess.readAll(); + response = response.trimmed(); + if(!response.contains("Utterance")) + { + qDebug() << "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() +{ + QString serverPath = settings->subValue("festival-server",RbSettings::TtsPath).toString(); + QString clientPath = settings->subValue("festival-client",RbSettings::TtsVoice).toString(); + + bool ret = QFileInfo(serverPath).isExecutable() && + QFileInfo(clientPath).isExecutable(); + if(settings->subValue("festival",RbSettings::TtsVoice).toString().size() > 0 && voices.size() > 0) + ret = ret && (voices.indexOf(settings->subValue("festival",RbSettings::TtsVoice).toString()) != -1); + return ret; +} + +QStringList TTSFestival::getVoiceList(QString path) +{ + if(!configOk()) + return QStringList(); + + if(voices.size() > 0) + { + qDebug() << "Using voice cache"; + return voices; + } + + QString response = queryServer("(voice.list)",3000,path); + + // 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) + qDebug() << "Voices: " << voices; + else + qDebug() << "No voices."; + + return voices; +} + +QString TTSFestival::getVoiceInfo(QString voice,QString path) +{ + if(!configOk()) + return ""; + + if(!getVoiceList().contains(voice)) + return ""; + + if(voiceDescriptions.contains(voice)) + return voiceDescriptions[voice]; + + QString response = queryServer(QString("(voice.description '%1)").arg(voice), 3000,path); + + if (response == "") + { + voiceDescriptions[voice]=tr("No description available"); + } + else + { + response = response.remove(QRegExp("(description \"*\")", Qt::CaseInsensitive, QRegExp::Wildcard)); + qDebug() << "voiceInfo w/o descr: " << response; + response = response.remove(')'); + QStringList responseLines = response.split('(', QString::SkipEmptyParts); + 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) + { + line = line.insert(firstSpace, ':'); // add a colon between the key and the value + line[firstSpace+2] = line[firstSpace+2].toUpper(); // capitalize the value + } + + description += line + "\n"; + } + voiceDescriptions[voice] = description.trimmed(); + } + + return voiceDescriptions[voice]; +} + +QString TTSFestival::queryServer(QString query, int timeout,QString path) +{ + if(!configOk()) + return ""; + + // this operation could take some time + emit busy(); + + ensureServerRunning(path); + + qDebug() << "queryServer with " << query; + QString response; + + QDateTime endTime; + if(timeout > 0) + 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. + */ + while(true) + { + QCoreApplication::processEvents(QEventLoop::AllEvents, 50); + QTcpSocket socket; + + socket.connectToHost("localhost", 1314); + socket.waitForConnected(); + + if(socket.state() == QAbstractSocket::ConnectedState) + { + socket.write(QString("%1\n").arg(query).toAscii()); + socket.waitForBytesWritten(); + socket.waitForReadyRead(); + + response = socket.readAll().trimmed(); + + if (response != "LP" && response != "") + break; + } + socket.abort(); + socket.disconnectFromHost(); + + if(timeout > 0 && QDateTime::currentDateTime() >= endTime) + { + emit busyEnd(); + return ""; + } + /* 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); + } + if(response == "nil") + { + emit busyEnd(); + return ""; + } + + QStringList lines = response.split('\n'); + if(lines.size() > 2) + { + lines.removeFirst(); + lines.removeLast(); + } + else + qDebug() << "Response too short: " << response; + + emit busyEnd(); + return lines.join("\n"); + +} + |