diff options
author | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
---|---|---|
committer | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
commit | 00bb99ac80741fc50ef8a289719373032f2391eb (patch) | |
tree | 3a5a9bf72f942784b38bf77dd66c534662fab5f2 /kttsd/kttsd | |
download | tdeaccessibility-00bb99ac80741fc50ef8a289719373032f2391eb.tar.gz tdeaccessibility-00bb99ac80741fc50ef8a289719373032f2391eb.zip |
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdeaccessibility@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kttsd/kttsd')
-rw-r--r-- | kttsd/kttsd/Makefile.am | 51 | ||||
-rw-r--r-- | kttsd/kttsd/SSMLtoPlainText.xsl | 9 | ||||
-rw-r--r-- | kttsd/kttsd/filtermgr.cpp | 405 | ||||
-rw-r--r-- | kttsd/kttsd/filtermgr.h | 199 | ||||
-rw-r--r-- | kttsd/kttsd/kttsd.cpp | 1183 | ||||
-rw-r--r-- | kttsd/kttsd/kttsd.desktop | 56 | ||||
-rw-r--r-- | kttsd/kttsd/kttsd.h | 686 | ||||
-rw-r--r-- | kttsd/kttsd/main.cpp | 68 | ||||
-rw-r--r-- | kttsd/kttsd/speaker.cpp | 1701 | ||||
-rw-r--r-- | kttsd/kttsd/speaker.h | 599 | ||||
-rw-r--r-- | kttsd/kttsd/speechdata.cpp | 1275 | ||||
-rw-r--r-- | kttsd/kttsd/speechdata.h | 731 | ||||
-rw-r--r-- | kttsd/kttsd/ssmlconvert.cpp | 295 | ||||
-rw-r--r-- | kttsd/kttsd/ssmlconvert.h | 129 | ||||
-rw-r--r-- | kttsd/kttsd/talkermgr.cpp | 388 | ||||
-rw-r--r-- | kttsd/kttsd/talkermgr.h | 159 | ||||
-rw-r--r-- | kttsd/kttsd/threadedplugin.cpp | 282 | ||||
-rw-r--r-- | kttsd/kttsd/threadedplugin.h | 200 |
18 files changed, 8416 insertions, 0 deletions
diff --git a/kttsd/kttsd/Makefile.am b/kttsd/kttsd/Makefile.am new file mode 100644 index 0000000..a553636 --- /dev/null +++ b/kttsd/kttsd/Makefile.am @@ -0,0 +1,51 @@ +# Include paths. INCLUDES is maintained by KDevelop, AM_CPPFLAGS is the preferred variable, +# so keep them synchronized. +INCLUDES = \ + -I$(top_srcdir)/kttsd/libkttsd \ + -I$(kde_includes)/arts \ + $(KTTS_KSPEECH_INCLUDE) \ + $(all_includes) + +# Let am_edit/unsermake handle all of the metasource files (moc). +METASOURCES = AUTO + +######################################################################### +# APPLICATION SECTION +######################################################################### +# This is the program that gets installed. It's name is used for all +# of the other Makefile.am variables. +bin_PROGRAMS = kttsd + +kspeech_DIR = $(KTTS_KSPEECH_DIR) +kspeechsink_DIR = $(KTTS_KSPEECH_DIR) + +# The source, library search path, and link libraries. +# Note: .skel files cause DCOPIDL compiler to generate _skel.cpp file and compile it. +kttsd_SOURCES = \ + kspeech.skel kspeechsink.stub\ + main.cpp \ + kttsd.cpp \ + speaker.cpp \ + speechdata.cpp \ + kttsd.skel \ + threadedplugin.cpp \ + ssmlconvert.cpp \ + filtermgr.cpp \ + talkermgr.cpp + +kttsd_LDFLAGS = -avoid-version -module $(all_libraries) $(KDE_RPATH) +kttsd_LDADD = \ + $(top_builddir)/kttsd/libkttsd/libkttsd.la \ + $(LIB_KDECORE) \ + $(LIB_KIO) \ + $(LIB_KUTILS) + +# Install desktop file to standard services directory. +kde_services_DATA = kttsd.desktop + +# Install data. +kttsddatadir = $(kde_datadir)/kttsd/xslt/ +kttsddata_DATA = SSMLtoPlainText.xsl + +noinst_HEADERS = threadedplugin.h ssmlconvert.h + diff --git a/kttsd/kttsd/SSMLtoPlainText.xsl b/kttsd/kttsd/SSMLtoPlainText.xsl new file mode 100644 index 0000000..c1c087d --- /dev/null +++ b/kttsd/kttsd/SSMLtoPlainText.xsl @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:output method="text" encoding="ISO-8859-1" indent="no"/> + +<xsl:template match="speak"> +<xsl:value-of select="."/> +</xsl:template> + +</xsl:stylesheet> diff --git a/kttsd/kttsd/filtermgr.cpp b/kttsd/kttsd/filtermgr.cpp new file mode 100644 index 0000000..3b0474b --- /dev/null +++ b/kttsd/kttsd/filtermgr.cpp @@ -0,0 +1,405 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Description: + Filters text, applying each configured Filter in turn. + Runs asynchronously, emitting Finished() signal when all Filters have run. + + Copyright: + (C) 2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Gary Cramblitt <garycramblitt@comcast.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// KDE includes. +#include <kdebug.h> +#include <kconfig.h> +#include <ktrader.h> +#include <kparts/componentfactory.h> +#include <klocale.h> + +// FilterMgr includes. +#include "filtermgr.h" +#include "filtermgr.moc" + +/** + * Constructor. + */ +FilterMgr::FilterMgr( QObject *parent, const char *name) : + KttsFilterProc(parent, name) +{ + // kdDebug() << "FilterMgr::FilterMgr: Running" << endl; + m_state = fsIdle; + m_noSBD = false; + m_supportsHTML = false; + m_talkerCode = 0; +} + +/** + * Destructor. + */ +FilterMgr::~FilterMgr() +{ + // kdDebug() << "FilterMgr::~FilterMgr: Running" << endl; + if ( m_state == fsFiltering ) + stopFiltering(); + m_filterList.setAutoDelete( TRUE ); + m_filterList.clear(); +} + +/** + * Loads and initializes the filters. + * @param config Settings object. + * @return False if FilterMgr is not ready to filter. + */ +bool FilterMgr::init(KConfig *config, const QString& /*configGroup*/) +{ + // Load each of the filters and initialize. + config->setGroup("General"); + QStringList filterIDsList = config->readListEntry("FilterIDs", ','); + // kdDebug() << "FilterMgr::init: FilterIDs = " << filterIDsList << endl; + // If no filters have been configured, automatically configure the standard SBD. + if (filterIDsList.isEmpty()) + { + config->setGroup("Filter_1"); + config->writeEntry("DesktopEntryName", "kttsd_sbdplugin"); + config->writeEntry("Enabled", true); + config->writeEntry("IsSBD", true); + config->writeEntry("MultiInstance", true); + config->writeEntry("SentenceBoundary", "\\1\\t"); + config->writeEntry("SentenceDelimiterRegExp", "([\\.\\?\\!\\:\\;])(\\s|$|(\\n *\\n))"); + config->writeEntry("UserFilterName", i18n("Standard Sentence Boundary Detector")); + config->setGroup("General"); + config->writeEntry("FilterIDs", "1"); + filterIDsList = config->readListEntry("FilterIDs", ','); + } + if ( !filterIDsList.isEmpty() ) + { + QStringList::ConstIterator itEnd = filterIDsList.constEnd(); + for (QStringList::ConstIterator it = filterIDsList.constBegin(); it != itEnd; ++it) + { + QString filterID = *it; + QString groupName = "Filter_" + filterID; + config->setGroup( groupName ); + QString desktopEntryName = config->readEntry( "DesktopEntryName" ); + // If a DesktopEntryName is not in the config file, it was configured before + // we started using them, when we stored translated plugin names instead. + // Try to convert the translated plugin name to a DesktopEntryName. + // DesktopEntryNames are better because user can change their desktop language + // and DesktopEntryName won't change. + if (desktopEntryName.isEmpty()) + { + QString filterPlugInName = config->readEntry("PlugInName", QString::null); + // See if the translated name will untranslate. If not, well, sorry. + desktopEntryName = FilterNameToDesktopEntryName(filterPlugInName); + // Record the DesktopEntryName from now on. + if (!desktopEntryName.isEmpty()) config->writeEntry("DesktopEntryName", desktopEntryName); + } + if (config->readBoolEntry("Enabled") || config->readBoolEntry("IsSBD")) + { + // kdDebug() << "FilterMgr::init: filterID = " << filterID << endl; + KttsFilterProc* filterProc = loadFilterPlugin( desktopEntryName ); + if ( filterProc ) + { + filterProc->init( config, groupName ); + m_filterList.append( filterProc ); + } + if (config->readEntry("DocType").contains("html") || + config->readEntry("RootElement").contains("html")) + m_supportsHTML = true; + } + } + } + return true; +} + +/** + * Returns True if this filter is a Sentence Boundary Detector. + * If so, the filter should implement @ref setSbRegExp() . + * @return True if this filter is a SBD. + */ +/*virtual*/ bool FilterMgr::isSBD() { return true; } + +/** + * Returns True if the plugin supports asynchronous processing, + * i.e., supports asyncConvert method. + * @return True if this plugin supports asynchronous processing. + * + * If the plugin returns True, it must also implement @ref getState . + * It must also emit @ref filteringFinished when filtering is completed. + * If the plugin returns True, it must also implement @ref stopFiltering . + * It must also emit @ref filteringStopped when filtering has been stopped. + */ +/*virtual*/ bool FilterMgr::supportsAsync() { return true; } + +/** + * Synchronously convert text. + * @param inputText Input text. + * @param talkerCode TalkerCode structure for the talker that KTTSD intends to + * use for synthing the text. Useful for extracting hints about + * how to filter the text. For example, languageCode. + * @param appId The DCOP appId of the application that queued the text. + * Also useful for hints about how to do the filtering. + * @return Converted text. + */ +QString FilterMgr::convert(const QString& inputText, TalkerCode* talkerCode, const QCString& appId) +{ + m_text = inputText; + m_talkerCode = talkerCode; + m_appId = appId; + m_filterIndex = -1; + m_filterProc = 0; + m_state = fsFiltering; + m_async = false; + while ( m_state == fsFiltering ) + nextFilter(); + return m_text; +} + +/** + * Aynchronously convert input. + * @param inputText Input text. + * @param talkerCode TalkerCode structure for the talker that KTTSD intends to + * use for synthing the text. Useful for extracting hints about + * how to filter the text. For example, languageCode. + * @param appId The DCOP appId of the application that queued the text. + * Also useful for hints about how to do the filtering. + * + * When the input text has been converted, filteringFinished signal will be emitted + * and caller can retrieve using getOutput(); +*/ +bool FilterMgr::asyncConvert(const QString& inputText, TalkerCode* talkerCode, const QCString& appId) +{ + m_text = inputText; + m_talkerCode = talkerCode; + m_appId = appId; + m_filterIndex = -1; + m_filterProc = 0; + m_state = fsFiltering; + m_async = true; + nextFilter(); + return true; +} + +// Finishes up with current filter (if any) and goes on to the next filter. +void FilterMgr::nextFilter() +{ + if ( m_filterProc ) + { + if ( m_filterProc->supportsAsync() ) + { + m_text = m_filterProc->getOutput(); + m_filterProc->ackFinished(); + disconnect( m_filterProc, SIGNAL(filteringFinished()), this, SLOT(slotFilteringFinished()) ); + } + // if ( m_filterProc->wasModified() ) + // kdDebug() << "FilterMgr::nextFilter: Filter# " << m_filterIndex << " modified the text." << endl; + if ( m_filterProc->wasModified() && m_filterProc->isSBD() ) + { + m_state = fsFinished; + // Post an event which will be later emitted as a signal. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 301); + QApplication::postEvent(this, ev); + return; + } + } + ++m_filterIndex; + if ( m_filterIndex == static_cast<int>(m_filterList.count()) ) + { + m_state = fsFinished; + // Post an event which will be later emitted as a signal. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 301); + QApplication::postEvent(this, ev); + return; + } + m_filterProc = m_filterList.at(m_filterIndex); + if ( m_noSBD && m_filterProc->isSBD() ) + { + m_state = fsFinished; + // Post an event which will be later emitted as a signal. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 301); + QApplication::postEvent(this, ev); + return; + } + m_filterProc->setSbRegExp( m_re ); + if ( m_async ) + { + if ( m_filterProc->supportsAsync() ) + { + // kdDebug() << "FilterMgr::nextFilter: calling asyncConvert on filter " << m_filterIndex << endl; + connect( m_filterProc, SIGNAL(filteringFinished()), this, SLOT(slotFilteringFinished()) ); + if ( !m_filterProc->asyncConvert( m_text, m_talkerCode, m_appId ) ) + { + disconnect( m_filterProc, SIGNAL(filteringFinished()), this, SLOT(slotFilteringFinished()) ); + m_filterProc = 0; + nextFilter(); + } + } else { + m_text = m_filterProc->convert( m_text, m_talkerCode, m_appId ); + nextFilter(); + } + } else + m_text = m_filterProc->convert( m_text, m_talkerCode, m_appId ); +} + +// Received when each filter finishes. +void FilterMgr::slotFilteringFinished() +{ + // kdDebug() << "FilterMgr::slotFilteringFinished: received signal from filter " << m_filterIndex << endl; + nextFilter(); +} + +bool FilterMgr::event ( QEvent * e ) +{ + if ( e->type() == (QEvent::User + 301) ) + { + // kdDebug() << "FilterMgr::event: emitting filteringFinished signal." << endl; + emit filteringFinished(); + return true; + } + if ( e->type() == (QEvent::User + 302) ) + { + // kdDebug() << "FilterMgr::event: emitting filteringStopped signal." << endl; + emit filteringStopped(); + return true; + } + else return false; +} + +/** + * Waits for filtering to finish. + */ +void FilterMgr::waitForFinished() +{ + if ( m_state != fsFiltering ) return; + disconnect(m_filterProc, SIGNAL(filteringFinished()), this, SLOT(slotFilteringFinished()) ); + m_async = false; + m_filterProc->waitForFinished(); + while ( m_state == fsFiltering ) + nextFilter(); +} + +/** + * Returns the state of the FilterMgr. + */ +int FilterMgr::getState() { return m_state; } + +/** + * Returns the filtered output. + */ +QString FilterMgr::getOutput() +{ + return m_text; +} + +/** + * Acknowledges the finished filtering. + */ +void FilterMgr::ackFinished() +{ + m_state = fsIdle; + m_text = QString::null; +} + +/** + * Stops filtering. The filteringStopped signal will emit when filtering + * has in fact stopped. + */ +void FilterMgr::stopFiltering() +{ + if ( m_state != fsFiltering ) return; + if ( m_async ) + disconnect( m_filterProc, SIGNAL(filteringFinished()), this, SLOT(slotFilteringFinished()) ); + m_filterProc->stopFiltering(); + m_state = fsIdle; + QCustomEvent* ev = new QCustomEvent(QEvent::User + 302); + QApplication::postEvent(this, ev); +} + +/** + * Set Sentence Boundary Regular Expression. + * This method will only be called if the application overrode the default. + * + * @param re The sentence delimiter regular expression. + */ +/*virtual*/ void FilterMgr::setSbRegExp(const QString& re) +{ + m_re = re; +} + +/** + * Do not call SBD filters. + */ +void FilterMgr::setNoSBD(bool noSBD) { m_noSBD = noSBD; } +bool FilterMgr::noSBD() { return m_noSBD; } + +// Loads the processing plug in for a filter plug in given its DesktopEntryName. +KttsFilterProc* FilterMgr::loadFilterPlugin(const QString& desktopEntryName) +{ + // kdDebug() << "FilterMgr::loadFilterPlugin: Running"<< endl; + + // Find the plugin. + KTrader::OfferList offers = KTrader::self()->query("KTTSD/FilterPlugin", + QString("DesktopEntryName == '%1'").arg(desktopEntryName)); + + if (offers.count() == 1) + { + // When the entry is found, load the plug in + // First create a factory for the library + KLibFactory *factory = KLibLoader::self()->factory(offers[0]->library().latin1()); + if(factory){ + // If the factory is created successfully, instantiate the KttsFilterConf class for the + // specific plug in to get the plug in configuration object. + int errorNo; + KttsFilterProc *plugIn = + KParts::ComponentFactory::createInstanceFromLibrary<KttsFilterProc>( + offers[0]->library().latin1(), NULL, offers[0]->library().latin1(), + QStringList(), &errorNo); + if(plugIn){ + // If everything went ok, return the plug in pointer. + return plugIn; + } else { + // Something went wrong, returning null. + kdDebug() << "FilterMgr::loadFilterPlugin: Unable to instantiate KttsFilterProc class for plugin " << desktopEntryName << " error: " << errorNo << endl; + return NULL; + } + } else { + // Something went wrong, returning null. + kdDebug() << "FilterMgr::loadFilterPlugin: Unable to create Factory object for plugin " + << desktopEntryName << endl; + return NULL; + } + } + // The plug in was not found (unexpected behaviour, returns null). + kdDebug() << "FilterMgr::loadFilterPlugin: KTrader did not return an offer for plugin " + << desktopEntryName << endl; + return NULL; +} + +/** + * Uses KTrader to convert a translated Filter Plugin Name to DesktopEntryName. + * @param name The translated plugin name. From Name= line in .desktop file. + * @return DesktopEntryName. The name of the .desktop file (less .desktop). + * QString::null if not found. + */ +QString FilterMgr::FilterNameToDesktopEntryName(const QString& name) +{ + if (name.isEmpty()) return QString::null; + KTrader::OfferList offers = KTrader::self()->query("KTTSD/FilterPlugin"); + for (uint ndx = 0; ndx < offers.count(); ++ndx) + if (offers[ndx]->name() == name) return offers[ndx]->desktopEntryName(); + return QString::null; +} + diff --git a/kttsd/kttsd/filtermgr.h b/kttsd/kttsd/filtermgr.h new file mode 100644 index 0000000..d909128 --- /dev/null +++ b/kttsd/kttsd/filtermgr.h @@ -0,0 +1,199 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Description: + Filters text, applying each configured Filter in turn. + Runs asynchronously, emitting Finished() signal when all Filters have run. + + Copyright: + (C) 2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Gary Cramblitt <garycramblitt@comcast.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _FILTERMGR_H_ +#define _FILTERMGR_H_ + +// Qt includes. +#include <qptrlist.h> + +// KTTS includes. +#include "filterproc.h" + +class KConfig; +class TalkerCode; + +typedef QPtrList<KttsFilterProc> FilterList; + +class FilterMgr : public KttsFilterProc +{ + Q_OBJECT + + public: + /** + * Constructor. + */ + FilterMgr(QObject *parent = 0, const char *name = 0); + + /** + * Destructor. + */ + ~FilterMgr(); + + /** + * Initialize the filters. + * @param config Settings object. + * @param configGroup Settings Group. + * @return False if filter is not ready to filter. + * + * Note: The parameters are for reading from kttsdrc file. Plugins may wish to maintain + * separate configuration files of their own. + */ + virtual bool init(KConfig *config, const QString &configGroup); + + /** + * Returns True if this filter is a Sentence Boundary Detector. + * If so, the filter should implement @ref setSbRegExp() . + * @return True if this filter is a SBD. + */ + virtual bool isSBD(); + + /** + * Returns True if the plugin supports asynchronous processing, + * i.e., supports asyncConvert method. + * @return True if this plugin supports asynchronous processing. + * + * If the plugin returns True, it must also implement @ref getState . + * It must also emit @ref filteringFinished when filtering is completed. + * If the plugin returns True, it must also implement @ref stopFiltering . + * It must also emit @ref filteringStopped when filtering has been stopped. + */ + virtual bool supportsAsync(); + + /** + * Synchronously convert text. + * @param inputText Input text. + * @param talkerCode TalkerCode structure for the talker that KTTSD intends to + * use for synthing the text. Useful for extracting hints about + * how to filter the text. For example, languageCode. + * @param appId The DCOP appId of the application that queued the text. + * Also useful for hints about how to do the filtering. + * @return Converted text. + */ + virtual QString convert(const QString& inputText, TalkerCode* talkerCode, const QCString& appId); + + /** + * Asynchronously convert input. + * @param inputText Input text. + * @param talkerCode TalkerCode structure for the talker that KTTSD intends to + * use for synthing the text. Useful for extracting hints about + * how to filter the text. For example, languageCode. + * @param appId The DCOP appId of the application that queued the text. + * Also useful for hints about how to do the filtering. + * + * When the input text has been converted, filteringFinished signal will be emitted + * and caller can retrieve using getOutput(); + */ + virtual bool asyncConvert(const QString& inputText, TalkerCode* talkerCode, const QCString& appId); + + /** + * Waits for filtering to finish. + */ + virtual void waitForFinished(); + + /** + * Returns the state of the FilterMgr. + */ + virtual int getState(); + + /** + * Returns the filtered output. + */ + virtual QString getOutput(); + + /** + * Acknowledges the finished filtering. + */ + virtual void ackFinished(); + + /** + * Stops filtering. The filteringStopped signal will emit when filtering + * has in fact stopped. + */ + virtual void stopFiltering(); + + /** + * Set Sentence Boundary Regular Expression. + * This method will only be called if the application overrode the default. + * + * @param re The sentence delimiter regular expression. + */ + virtual void setSbRegExp(const QString& re); + + /** + * Do not call SBD filters. + */ + void setNoSBD(bool noSBD); + bool noSBD(); + + /** + * True if there is at least one XML Transformer filter for html. + */ + bool supportsHTML() { return m_supportsHTML; } + + protected: + bool event ( QEvent * e ); + + private slots: + void slotFilteringFinished(); + + private: + // Loads the processing plug in for a named filter plug in. + KttsFilterProc* loadFilterPlugin(const QString& plugInName); + // Finishes up with current filter (if any) and goes on to the next filter. + void nextFilter(); + // Uses KTrader to convert a translated Filter Plugin Name to DesktopEntryName. + // @param name The translated plugin name. From Name= line in .desktop file. + // @return DesktopEntryName. The name of the .desktop file (less .desktop). + // QString::null if not found. + QString FilterNameToDesktopEntryName(const QString& name); + + // List of filters. + FilterList m_filterList; + // Text being filtered. + QString m_text; + // Index to list of filters. + int m_filterIndex; + // Current filter. + KttsFilterProc* m_filterProc; + // True if calling filters asynchronously. + bool m_async; + // Talker Code. + TalkerCode* m_talkerCode; + // AppId. + QCString m_appId; + // Sentence Boundary regular expression (if app overrode the default). + QString m_re; + // True if any of the filters modified the text. + bool m_wasModified; + // FilterMgr state. + int m_state; + // True if SBD Filters should not be called. + bool m_noSBD; + // True if at least one XML Transformer for html is enabled. + bool m_supportsHTML; +}; + +#endif // _FILTERMGR_H_ diff --git a/kttsd/kttsd/kttsd.cpp b/kttsd/kttsd/kttsd.cpp new file mode 100644 index 0000000..9ee841f --- /dev/null +++ b/kttsd/kttsd/kttsd.cpp @@ -0,0 +1,1183 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + KTTSD main class + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + Current Maintainer: Gary Cramblitt <garycramblitt@comcast.net> + ******************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; version 2 of the License. * + * * + ***************************************************************************/ + +// Qt includes. +#include <qcstring.h> +#include <qclipboard.h> +#include <qtextstream.h> +#include <qtextcodec.h> +#include <qfile.h> + +// KDE includes. +#include <kdebug.h> +#include <kapplication.h> +#include <kmessagebox.h> +#include <klocale.h> +#include <kfiledialog.h> +#include <dcopclient.h> +#include <knotifyclient.h> +#include <krun.h> +#include <kaboutdata.h> + +// KTTS includes. +#include "notify.h" +#include "kttsd.h" + +/** +* This is the "main" module of KTTSD. It performs the following functions: +* - Creates and destroys SpeechData and Speaker objects. +* - Receives DCOP calls and dispatches them to SpeechData and Speaker. +* - Receives signals from SpeechData and Speaker and converts them to DCOP signals. +* +* Note that most of the real tts work occurs in Speaker. +*/ + +KTTSD::KTTSD(const QCString& objId, QObject *parent, const char *name) : + DCOPObject(objId), + QObject(parent, name) +{ + // kdDebug() << "KTTSD::KTTSD Running" << endl; + m_speaker = 0; + m_talkerMgr = 0; + m_speechData = 0; + ready(); +} + +/* +* Create and initialize the SpeechData object. +*/ +bool KTTSD::initializeSpeechData() +{ + // Create speechData object. + if (!m_speechData) + { + m_speechData = new SpeechData(); + connect (m_speechData, SIGNAL(textSet(const QCString&, const uint)), + this, SLOT(slotTextSet(const QCString&, const uint))); + connect (m_speechData, SIGNAL(textAppended(const QCString&, const uint, const int)), + this, SLOT(slotTextAppended(const QCString&, const uint, const int))); + connect (m_speechData, SIGNAL(textRemoved(const QCString&, const uint)), + this, SLOT(slotTextRemoved(const QCString&, const uint))); + + // Hook KNotify signal. + if (!connectDCOPSignal(0, 0, + "notifySignal(QString,QString,QString,QString,QString,int,int,int,int)", + "notificationSignal(QString,QString,QString,QString,QString,int,int,int,int)", + false)) kdDebug() << "KTTSD:initializeSpeechData: connectDCOPSignal for knotify failed" << endl; + } + // Load configuration. + m_speechData->readConfig(); + + return true; +} + +/* +* Create and initialize the TalkerMgr object. +*/ +bool KTTSD::initializeTalkerMgr() +{ + if (!m_talkerMgr) + { + if (!m_speechData) initializeSpeechData(); + + m_talkerMgr = new TalkerMgr(this, "kttsdtalkermgr"); + int load = m_talkerMgr->loadPlugIns(m_speechData->config); + // If no Talkers configured, try to autoconfigure one, first in the user's + // desktop language, but if that fails, fallback to English. + if (load < 0) + { + QString languageCode = KGlobal::locale()->language(); + if (m_talkerMgr->autoconfigureTalker(languageCode, m_speechData->config)) + load = m_talkerMgr->loadPlugIns(m_speechData->config); + else + { + if (m_talkerMgr->autoconfigureTalker("en", m_speechData->config)) + load = m_talkerMgr->loadPlugIns(m_speechData->config); + } + } + if (load < 0) + { + // TODO: Would really like to eliminate ALL GUI stuff from kttsd. Find + // a better way to do this. + delete m_speaker; + m_speaker = 0; + delete m_talkerMgr; + m_talkerMgr = 0; + delete m_speechData; + m_speechData = 0; + kdDebug() << "KTTSD::initializeTalkerMgr: no Talkers have been configured." << endl; + // Ask if user would like to run configuration dialog, but don't bug user unnecessarily. + QString dontAskConfigureKTTS = "DontAskConfigureKTTS"; + KMessageBox::ButtonCode msgResult; + if (KMessageBox::shouldBeShownYesNo(dontAskConfigureKTTS, msgResult)) + { + if (KMessageBox::questionYesNo( + 0, + i18n("KTTS has not yet been configured. At least one Talker must be configured. " + "Would you like to configure it now?"), + i18n("KTTS Not Configured"), + i18n("Configure"), + i18n("Do Not Configure"), + dontAskConfigureKTTS) == KMessageBox::Yes) msgResult = KMessageBox::Yes; + } + if (msgResult == KMessageBox::Yes) showDialog(); + return false; + } + } + m_speechData->setTalkerMgr(m_talkerMgr); + return true; +} + +/* +* Create and initialize the Speaker object. +*/ +bool KTTSD::initializeSpeaker() +{ + // kdDebug() << "KTTSD::initializeSpeaker: Instantiating Speaker" << endl; + + if (!m_talkerMgr) initializeTalkerMgr(); + + // Create speaker object and load plug ins, checking for the return + m_speaker = new Speaker(m_speechData, m_talkerMgr); + connect (m_speaker, SIGNAL(textStarted(const QCString&, const uint)), + this, SLOT(slotTextStarted(const QCString&, const uint))); + connect (m_speaker, SIGNAL(textFinished(const QCString&, const uint)), + this, SLOT(slotTextFinished(const QCString&, const uint))); + connect (m_speaker, SIGNAL(textResumed(const QCString&, const uint)), + this, SLOT(slotTextResumed(const QCString&, const uint))); + connect (m_speaker, SIGNAL(sentenceStarted(QString, QString, const QCString&, const uint, const uint)), + this, SLOT(slotSentenceStarted(QString, QString, const QCString&, const uint, const uint))); + connect (m_speaker, SIGNAL(sentenceFinished(const QCString&, const uint, const uint)), this, + SLOT(slotSentenceFinished(const QCString&, const uint, const uint))); + connect (m_speaker, SIGNAL(textStopped(const QCString&, const uint)), + this, SLOT(slotTextStopped(const QCString&, const uint))); + connect (m_speaker, SIGNAL(textPaused(const QCString&, const uint)), + this, SLOT(slotTextPaused(const QCString&, const uint))); + + return true; +} + +/** + * Destructor + * Terminate speaker thread + */ +KTTSD::~KTTSD(){ + kdDebug() << "KTTSD::~KTTSD:: Stopping KTTSD service" << endl; + if (m_speaker) m_speaker->requestExit(); + delete m_speaker; + delete m_talkerMgr; + delete m_speechData; + kdDebug() << "KTTSD::~KTTSD: Emitting DCOP signal kttsdExiting()" << endl; + kttsdExiting(); +} + +/***** DCOP exported functions *****/ + +/** +* Determine whether the currently-configured speech plugin supports a speech markup language. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* @param markupType The kttsd code for the desired speech markup language. +* @return True if the plugin currently configured for the indicated +* talker supports the indicated speech markup language. +* @see kttsdMarkupType +*/ +bool KTTSD::supportsMarkup(const QString& talker /*=NULL*/, const uint markupType /*=0*/) const +{ + if (markupType == KSpeech::mtHtml) + { + if (!m_speechData) return false; + return m_speechData->supportsHTML; + } + if (markupType != KSpeech::mtSsml) return false; + if (!m_talkerMgr) return false; + return m_talkerMgr->supportsMarkup(fixNullString(talker), markupType); +} + +/** +* Determine whether the currently-configured speech plugin supports markers in speech markup. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* @return True if the plugin currently configured for the indicated +* talker supports markers. +* TODO: Waiting on plugin API. +*/ +bool KTTSD::supportsMarkers(const QString& /*talker=NULL*/) const { return false; } + +/** +* Say a message as soon as possible, interrupting any other speech in progress. +* IMPORTANT: This method is reserved for use by Screen Readers and should not be used +* by any other applications. +* @param msg The message to be spoken. +* @param talker Code for the to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +* +* If an existing Screen Reader output is in progress, it is stopped and discarded and +* replaced with this new message. +*/ +void KTTSD::sayScreenReaderOutput(const QString &msg, const QString &talker /*=NULL*/) +{ + if (!m_speaker) return; + m_speechData->setScreenReaderOutput(msg, fixNullString(talker), getAppId()); + m_speaker->doUtterances(); +} + +/** +* Say a warning. The warning will be spoken when the current sentence +* stops speaking and takes precedence over Messages and regular text. Warnings should only +* be used for high-priority messages requiring immediate user attention, such as +* "WARNING. CPU is overheating." +* @param warning The warning to be spoken. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +*/ +void KTTSD::sayWarning(const QString &warning, const QString &talker /*=NULL*/){ + // kdDebug() << "KTTSD::sayWarning: Running" << endl; + if (!m_speaker) return; + kdDebug() << "KTTSD::sayWarning: Adding '" << warning << "' to warning queue." << endl; + m_speechData->enqueueWarning(warning, fixNullString(talker), getAppId()); + m_speaker->doUtterances(); +} + +/** +* Say a message. The message will be spoken when the current sentence stops speaking +* but after any warnings have been spoken. +* Messages should be used for one-shot messages that can't wait for +* normal text messages to stop speaking, such as "You have mail.". +* @param message The message to be spoken. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no talker has been configured for the specified Talker code, +* defaults to the closest matching talker. +*/ +void KTTSD::sayMessage(const QString &message, const QString &talker /*=NULL*/) +{ + // kdDebug() << "KTTSD::sayMessage: Running" << endl; + if (!m_speaker) return; + kdDebug() << "KTTSD::sayMessage: Adding '" << message << "' to message queue." << endl; + m_speechData->enqueueMessage(message, fixNullString(talker), getAppId()); + m_speaker->doUtterances(); +} + +/** +* Sets the GREP pattern that will be used as the sentence delimiter. +* @param delimiter A valid GREP pattern. +* +* The default sentence delimiter is + @verbatim + ([\\.\\?\\!\\:\\;])\\s + @endverbatim +* +* Note that backward slashes must be escaped. +* +* Changing the sentence delimiter does not affect other applications. +* @see sentenceparsing +*/ +void KTTSD::setSentenceDelimiter(const QString &delimiter) +{ + if (!m_speaker) return; + m_speechData->setSentenceDelimiter(fixNullString(delimiter), getAppId()); +} + +/** +* Queue a text job. Does not start speaking the text. +* @param text The message to be spoken. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default plugin. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +* @return Job number. +* +* Plain text is parsed into individual sentences using the current sentence delimiter. +* Call @ref setSentenceDelimiter to change the sentence delimiter prior to calling setText. +* Call @ref getTextCount to retrieve the sentence count after calling setText. +* +* The text may contain speech mark language, such as Sable, JSML, or SMML, +* provided that the speech plugin/engine support it. In this case, +* sentence parsing follows the semantics of the markup language. +* +* Call @ref startText to mark the job as speakable and if the +* job is the first speakable job in the queue, speaking will begin. +* @see getTextCount +* @see startText +*/ +uint KTTSD::setText(const QString &text, const QString &talker /*=NULL*/) +{ + // kdDebug() << "KTTSD::setText: Running" << endl; + if (!m_speaker) return 0; + // kdDebug() << "KTTSD::setText: Setting text: '" << text << "'" << endl; + uint jobNum = m_speechData->setText(text, fixNullString(talker), getAppId()); + return jobNum; +} + +/** +* Say a plain text job. This is a convenience method that +* combines @ref setText and @ref startText into a single call. +* @param text The message to be spoken. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default plugin. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +* @return Job number. +* +* Plain text is parsed into individual sentences using the current sentence delimiter. +* Call @ref setSentenceDelimiter to change the sentence delimiter prior to +* calling setText. +* Call @ref getTextCount to retrieve the sentence count after calling setText. +* +* The text may contain speech mark language, such as Sable, JSML, or SSML, +* provided that the speech plugin/engine support it. In this case, +* sentence parsing follows the semantics of the markup language. +* +* The job is marked speakable. +* If there are other speakable jobs preceeding this one in the queue, +* those jobs continue speaking and when finished, this job will begin speaking. +* If there are no other speakable jobs preceeding this one, it begins speaking. +* +* @see getTextCount +* +* @since KDE 3.5 +*/ +uint KTTSD::sayText(const QString &text, const QString &talker) +{ + uint jobNum = setText(text, talker); + if (jobNum) startText(jobNum); + return jobNum; +} + +/** +* Adds another part to a text job. Does not start speaking the text. +* (thread safe) +* @param text The message to be spoken. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application, +* but if no such job, applies to the last job queued by any application. +* @return Part number for the added part. Parts are numbered starting at 1. +* +* The text is parsed into individual sentences. Call getTextCount to retrieve +* the sentence count. Call startText to mark the job as speakable and if the +* job is the first speakable job in the queue, speaking will begin. +* @see setText. +* @see startText. +*/ +int KTTSD::appendText(const QString &text, const uint jobNum /*=0*/) +{ + if (!m_speaker) return 0; + return m_speechData->appendText(text, applyDefaultJobNum(jobNum), getAppId()); +} + +/** +* Queue a text job from the contents of a file. Does not start speaking the text. +* @param filename Full path to the file to be spoken. May be a URL. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +* @param encoding Name of the encoding to use when reading the file. If +* NULL or Empty, uses default stream encoding. +* @return Job number. 0 if an error occurs. +* +* Plain text is parsed into individual sentences using the current sentence delimiter. +* Call @ref setSentenceDelimiter to change the sentence delimiter prior to calling setText. +* Call @ref getTextCount to retrieve the sentence count after calling setText. +* +* The text may contain speech mark language, such as Sable, JSML, or SMML, +* provided that the speech plugin/engine support it. In this case, +* sentence parsing follows the semantics of the markup language. +* +* Call @ref startText to mark the job as speakable and if the +* job is the first speakable job in the queue, speaking will begin. +* @see getTextCount +* @see startText +*/ +uint KTTSD::setFile(const QString &filename, const QString &talker /*=NULL*/, + const QString &encoding /*=NULL*/) +{ + // kdDebug() << "KTTSD::setFile: Running" << endl; + if (!m_speaker) return 0; + QFile file(filename); + uint jobNum = 0; + if ( file.open(IO_ReadOnly) ) + { + QTextStream stream(&file); + QString enc = fixNullString(encoding); + if (!enc.isEmpty()) + { + QTextCodec* codec = QTextCodec::codecForName(enc.latin1()); + if (codec) stream.setCodec(codec); + } + jobNum = m_speechData->setText(stream.read(), fixNullString(talker), getAppId()); + file.close(); + } + return jobNum; +} + +/** +* Get the number of sentences in a text job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* @return The number of sentences in the job. -1 if no such job. +* +* The sentences of a job are given sequence numbers from 1 to the number returned by this +* method. The sequence numbers are emitted in the @ref sentenceStarted and +* @ref sentenceFinished signals. +*/ +int KTTSD::getTextCount(const uint jobNum /*=0*/) +{ + if (!m_speaker) return -1; + return m_speechData->getTextCount(applyDefaultJobNum(jobNum)); +} + +/** +* Get the job number of the current text job. +* @return Job number of the current text job. 0 if no jobs. +* +* Note that the current job may not be speaking. See @ref isSpeakingText. +* @see getTextJobState. +* @see isSpeakingText +*/ +uint KTTSD::getCurrentTextJob() +{ + if (!m_speaker) return 0; + return m_speaker->getCurrentTextJob(); +} + +/** +* Get the number of jobs in the text job queue. +* @return Number of text jobs in the queue. 0 if none. +*/ +uint KTTSD::getTextJobCount() +{ + if (!m_speaker) return 0; + return m_speechData->getTextJobCount(); +} + +/** +* Get a comma-separated list of text job numbers in the queue. +* @return Comma-separated list of text job numbers in the queue. +*/ +QString KTTSD::getTextJobNumbers() +{ + if (!m_speaker) return QString::null; + return m_speechData->getTextJobNumbers(); +} + +/** +* Get the state of a text job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* @return State of the job. -1 if invalid job number. +* +* @see kttsdJobState +*/ +int KTTSD::getTextJobState(const uint jobNum /*=0*/) +{ + if (!m_speaker) return -1; + return m_speechData->getTextJobState(applyDefaultJobNum(jobNum)); +} + +/** +* Get information about a text job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* @return A QDataStream containing information about the job. +* Blank if no such job. +* +* The stream contains the following elements: +* - int state Job state. +* - QCString appId DCOP senderId of the application that requested the speech job. +* - QString talker Language code in which to speak the text. +* - int seq Current sentence being spoken. Sentences are numbered starting at 1. +* - int sentenceCount Total number of sentences in the job. +* +* The following sample code will decode the stream: + @verbatim + QByteArray jobInfo = getTextJobInfo(jobNum); + QDataStream stream(jobInfo, IO_ReadOnly); + int state; + QCString appId; + QString talker; + int seq; + int sentenceCount; + stream >> state; + stream >> appId; + stream >> talker; + stream >> seq; + stream >> sentenceCount; + @endverbatim +*/ +QByteArray KTTSD::getTextJobInfo(const uint jobNum /*=0*/) +{ + return m_speechData->getTextJobInfo(applyDefaultJobNum(jobNum)); +} + +/** +* Given a Talker Code, returns the Talker ID of the talker that would speak +* a text job with that Talker Code. +* @param talkerCode Talker Code. +* @return Talker ID of the talker that would speak the text job. +*/ +QString KTTSD::talkerCodeToTalkerId(const QString& talkerCode) +{ + if (!m_talkerMgr) return QString::null; + return m_talkerMgr->talkerCodeToTalkerId(fixNullString(talkerCode)); +} + +/** +* Return a sentence of a job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* @param seq Sequence number of the sentence. +* @return The specified sentence in the specified job. If no such +* job or sentence, returns "". +*/ +QString KTTSD::getTextJobSentence(const uint jobNum /*=0*/, const uint seq /*=1*/) +{ + return m_speechData->getTextJobSentence(applyDefaultJobNum(jobNum), seq); +} + +/** +* Determine if kttsd is currently speaking any text jobs. +* @return True if currently speaking any text jobs. +*/ +bool KTTSD::isSpeakingText() const +{ + if (!m_speaker) return false; + return m_speaker->isSpeakingText(); +} + +/** +* Remove a text job from the queue. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* +* The job is deleted from the queue and the @ref textRemoved signal is emitted. +* +* If there is another job in the text queue, and it is marked speakable, +* that job begins speaking. +*/ +void KTTSD::removeText(const uint jobNum /*=0*/) +{ + kdDebug() << "KTTSD::removeText: Running" << endl; + if (!m_speaker) return; + m_speaker->removeText(applyDefaultJobNum(jobNum)); +} + +/** +* Start a text job at the beginning. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* +* Rewinds the job to the beginning. +* +* The job is marked speakable. +* If there are other speakable jobs preceeding this one in the queue, +* those jobs continue speaking and when finished, this job will begin speaking. +* If there are no other speakable jobs preceeding this one, it begins speaking. +* +* The @ref textStarted signal is emitted when the text job begins speaking. +* When all the sentences of the job have been spoken, the job is marked for deletion from +* the text queue and the @ref textFinished signal is emitted. +*/ +void KTTSD::startText(const uint jobNum /*=0*/) +{ + kdDebug() << "KTTSD::startText: Running" << endl; + if (!m_speaker) return; + // Determine if we are starting speech. + bool startingSpeech = !isSpeakingText(); + uint jNum = applyDefaultJobNum(jobNum); + m_speaker->startText(jNum); + // If this has started speech output, determine whether to autostart KTTSMgr. + if (startingSpeech) + { + if (m_speechData->autoStartManager) + { + // Do not start KTTSMgr unless at least 5 sentences are queued. + if (getTextCount(jNum) > 4) + { + QString cmd = "kttsmgr --systray"; + if (m_speechData->autoExitManager) cmd.append(" --autoexit"); + // Notice this fails if KTTSMgr is already running, which is what we want. + KRun::runCommand(cmd); + } + } + } +} + +/** +* Stop a text job and rewind to the beginning. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* +* The job is marked not speakable and will not be speakable until @ref startText or @ref resumeText +* is called. +* +* If there are speaking jobs preceeding this one in the queue, they continue speaking. +* If the job is currently speaking, the @ref textStopped signal is emitted and the job stops speaking. +* Depending upon the speech engine and plugin used, speeking may not stop immediately +* (it might finish the current sentence). +*/ +void KTTSD::stopText(const uint jobNum /*=0*/) +{ + kdDebug() << "KTTSD::stopText: Running" << endl; + if (!m_speaker) return; + m_speaker->stopText(applyDefaultJobNum(jobNum)); +} + +/** +* Pause a text job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* +* The job is marked as paused and will not be speakable until @ref resumeText or +* @ref startText is called. +* +* If there are speaking jobs preceeding this one in the queue, they continue speaking. +* If the job is currently speaking, the @ref textPaused signal is emitted and the job stops speaking. +* Depending upon the speech engine and plugin used, speeking may not stop immediately +* (it might finish the current sentence). +* @see resumeText +*/ +void KTTSD::pauseText(const uint jobNum /*=0*/) +{ + kdDebug() << "KTTSD::pauseText: Running" << endl; + if (!m_speaker) return; + m_speaker->pauseText(applyDefaultJobNum(jobNum)); +} + +/** +* Start or resume a text job where it was paused. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* +* The job is marked speakable. +* +* If the job is currently speaking, or is waiting to be spoken (speakable +* state), the resumeText() call is ignored. +* +* If the job is currently queued, or is finished, it is the same as calling +* @ref startText . +* +* If there are speaking jobs preceeding this one in the queue, those jobs continue speaking and, +* when finished this job will begin speaking where it left off. +* +* The @ref textResumed signal is emitted when the job resumes. +* @see pauseText +*/ +void KTTSD::resumeText(const uint jobNum /*=0*/) +{ + kdDebug() << "KTTSD::resumeText: Running" << endl; + if (!m_speaker) return; + m_speaker->resumeText(applyDefaultJobNum(jobNum)); +} + +/** +* Get a list of the talkers configured in KTTS. +* @return A QStringList of fully-specified talker codes, one +* for each talker user has configured. +* +* @see talkers +*/ +QStringList KTTSD::getTalkers() +{ + if (!m_talkerMgr) return QStringList(); + return m_talkerMgr->getTalkers(); +} + +/** +* Change the talker for a text job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* @param talker New code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +*/ +void KTTSD::changeTextTalker(const QString &talker, uint jobNum) +{ + m_speechData->changeTextTalker(fixNullString(talker), applyDefaultJobNum(jobNum)); +} + +/** +* Get the user's default talker. +* @return A fully-specified talker code. +* +* @see talkers +* @see getTalkers +*/ +QString KTTSD::userDefaultTalker() +{ + if (!m_talkerMgr) return QString::null; + return m_talkerMgr->userDefaultTalker(); +} + +/** +* Move a text job down in the queue so that it is spoken later. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application. +* +* If the job is currently speaking, it is paused. +* If the next job in the queue is speakable, it begins speaking. +*/ +void KTTSD::moveTextLater(const uint jobNum /*=0*/) +{ + if (!m_speaker) return; + m_speaker->moveTextLater(applyDefaultJobNum(jobNum)); +} + +/** +* Jump to the first sentence of a specified part of a text job. +* @param partNum Part number of the part to jump to. Parts are numbered starting at 1. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application, +* but if no such job, applies to the last job queued by any application. +* @return Part number of the part actually jumped to. +* +* If partNum is greater than the number of parts in the job, jumps to last part. +* If partNum is 0, does nothing and returns the current part number. +* If no such job, does nothing and returns 0. +* Does not affect the current speaking/not-speaking state of the job. +*/ +int KTTSD::jumpToTextPart(const int partNum, const uint jobNum /*=0*/) +{ + if (!m_speaker) return 0; + return m_speaker->jumpToTextPart(partNum, applyDefaultJobNum(jobNum)); +} + +/** +* Advance or rewind N sentences in a text job. +* @param n Number of sentences to advance (positive) or rewind (negative) in the job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application, +* but if no such job, applies to the last job queued by any application. +* @return Sequence number of the sentence actually moved to. Sequence numbers +* are numbered starting at 1. +* +* If no such job, does nothing and returns 0. +* If n is zero, returns the current sequence number of the job. +* Does not affect the current speaking/not-speaking state of the job. +*/ +uint KTTSD::moveRelTextSentence(const int n, const uint jobNum /*=0*/) +{ + if (!m_speaker) return 0; + return m_speaker->moveRelTextSentence(n, applyDefaultJobNum(jobNum)); +} + +/** +* Add the clipboard contents to the text queue and begin speaking it. +*/ +void KTTSD::speakClipboard() +{ + // Get the clipboard object. + QClipboard *cb = kapp->clipboard(); + + // Copy text from the clipboard. + QString text = cb->text(); + + // Speak it. + if ( !text.isNull() ) + { + setText(text); + startText(); + } +} + +/** +* Displays the %KTTS Manager dialog. In this dialog, the user may backup or skip forward in +* any text job by sentence or paragraph, rewind jobs, pause or resume jobs, or +* delete jobs. +*/ +void KTTSD::showDialog() +{ + KRun::runCommand("kttsmgr"); +} + +/** +* Stop the service. +*/ +void KTTSD::kttsdExit() +{ + stopText(); + kdDebug() << "KTTSD::kttsdExit: Emitting DCOP signal kttsdExiting()" << endl; + kttsdExiting(); + kapp->quit(); +} + +/** +* Re-start %KTTSD. +*/ +void KTTSD::reinit() +{ + // Restart ourself. + kdDebug() << "KTTSD::reinit: Running" << endl; + if (m_speaker) + { + kdDebug() << "KTTSD::reinit: Stopping KTTSD service" << endl; + if (m_speaker->isSpeakingText()) pauseText(); + m_speaker->requestExit(); + } + delete m_speaker; + m_speaker = 0; + delete m_talkerMgr; + m_talkerMgr = 0; + ready(); +} + +/** +* Return KTTSD daemon version number. +*/ +QString KTTSD::version() { return kapp->aboutData()->version(); } + +/* +* Checks if KTTSD is ready to speak and at least one talker is configured. +* If not, user is prompted to display the configuration dialog. +*/ +bool KTTSD::ready() +{ + if (m_speaker) return true; + kdDebug() << "KTTSD::ready: Starting KTTSD service" << endl; + if (!initializeSpeechData()) return false; + if (!initializeTalkerMgr()) return false; + if (!initializeSpeaker()) return false; + m_speaker->doUtterances(); + kdDebug() << "KTTSD::ready: Emitting DCOP signal kttsdStarted()" << endl; + kttsdStarted(); + return true; +} + +void KTTSD::configCommitted() { + if (m_speaker) reinit(); +} + +/** +* This signal is emitted by KNotify when a notification event occurs. +* ds << event << fromApp << text << sound << file << present << level +* << winId << eventId; +* default_presentation contains these ORed events: None=0, Sound=1, Messagebox=2, Logfile=4, Stderr=8, +* PassivePopup=16, Execute=32, Taskbar=64 +*/ +void KTTSD::notificationSignal( const QString& event, const QString& fromApp, + const QString &text, const QString& sound, const QString& /*file*/, + const int present, const int /*level*/, const int /*windId*/, const int /*eventId*/) +{ + if (!m_speaker) return; + // kdDebug() << "KTTSD:notificationSignal: event: " << event << " fromApp: " << fromApp << + // " text: " << text << " sound: " << sound << " file: " << file << " present: " << present << + // " level: " << level << " windId: " << windId << " eventId: " << eventId << endl; + if ( m_speechData->notify ) + if ( !m_speechData->notifyExcludeEventsWithSound || sound.isEmpty() ) + { + bool found = false; + NotifyOptions notifyOptions; + QString msg; + QString talker; + // Check for app-specific action. + if ( m_speechData->notifyAppMap.contains( fromApp ) ) + { + NotifyEventMap notifyEventMap = m_speechData->notifyAppMap[ fromApp ]; + if ( notifyEventMap.contains( event ) ) + { + found = true; + notifyOptions = notifyEventMap[ event ]; + } else { + // Check for app-specific default. + if ( notifyEventMap.contains( "default" ) ) + { + found = true; + notifyOptions = notifyEventMap[ "default" ]; + notifyOptions.eventName = QString::null; + } + } + } + // If no app-specific action, try default. + if ( !found ) + { + switch ( m_speechData->notifyDefaultPresent ) + { + case NotifyPresent::None: + found = false; + break; + case NotifyPresent::Dialog: + found = ( + (present & KNotifyClient::Messagebox) + && + !(present & KNotifyClient::PassivePopup) + ); + break; + case NotifyPresent::Passive: + found = ( + !(present & KNotifyClient::Messagebox) + && + (present & KNotifyClient::PassivePopup) + ); + break; + case NotifyPresent::DialogAndPassive: + found = ( + (present & KNotifyClient::Messagebox) + && + (present & KNotifyClient::PassivePopup) + ); + break; + case NotifyPresent::All: + found = true; + break; + } + if ( found ) + notifyOptions = m_speechData->notifyDefaultOptions; + } + if ( found ) + { + int action = notifyOptions.action; + talker = notifyOptions.talker; + switch ( action ) + { + case NotifyAction::DoNotSpeak: + break; + case NotifyAction::SpeakEventName: + if (notifyOptions.eventName.isEmpty()) + msg = NotifyEvent::getEventName( fromApp, event ); + else + msg = notifyOptions.eventName; + break; + case NotifyAction::SpeakMsg: + msg = text; + break; + case NotifyAction::SpeakCustom: + msg = notifyOptions.customMsg; + msg.replace( "%a", fromApp ); + msg.replace( "%m", text ); + if ( msg.contains( "%e" ) ) + { + if ( notifyOptions.eventName.isEmpty() ) + msg.replace( "%e", NotifyEvent::getEventName( fromApp, event ) ); + else + msg.replace( "%e", notifyOptions.eventName ); + } + break; + } + } + // Queue msg if we should speak something. + if ( !msg.isEmpty() ) + { + QString fromApps = fromApp + ",knotify"; + m_speechData->enqueueMessage( msg, talker, fromApps.utf8() ); + m_speaker->doUtterances(); + } + } +} + +// Slots for the speaker object +void KTTSD::slotSentenceStarted(QString, QString, const QCString& appId, + const uint jobNum, const uint seq) { + // Emit DCOP signal. + kdDebug() << "KTTSD::slotSentenceStarted: Emitting DCOP signal sentenceStarted with appId " << appId << " job number " << jobNum << " seq number " << seq << endl; + sentenceStarted(appId, jobNum, seq); +} + +void KTTSD::slotSentenceFinished(const QCString& appId, const uint jobNum, const uint seq){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotSentenceFinished: Emitting DCOP signal sentenceFinished with appId " << appId << " job number " << jobNum << " seq number " << seq << endl; + sentenceFinished(appId, jobNum, seq); +} + +// Slots for the speechData and speaker objects. +void KTTSD::slotTextStarted(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextStarted: Emitting DCOP signal textStarted with appId " << appId << " job number " << jobNum << endl; + textStarted(appId, jobNum); +} + +void KTTSD::slotTextFinished(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextFinished: Emitting DCOP signal textFinished with appId " << appId << " job number " << jobNum << endl; + textFinished(appId, jobNum); +} + +void KTTSD::slotTextStopped(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextStopped: Emitting DCOP signal textStopped with appId " << appId << " job number " << jobNum << endl; + textStopped(appId, jobNum); +} + +void KTTSD::slotTextPaused(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextPaused: Emitting DCOP signal textPaused with appId " << appId << " job number " << jobNum << endl; + textPaused(appId, jobNum); +} + +void KTTSD::slotTextResumed(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextResumed: Emitting DCOP signal textResumed with appId " << appId << " job number " << jobNum << endl; + textResumed(appId, jobNum); +} + +//void KTTSD::slotTextSet(const QCString& appId, const uint jobNum){ +void KTTSD::slotTextSet(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextSet: Emitting DCOP signal textSet with appId " << appId << " job number " << jobNum << endl; + textSet(appId, jobNum); +} + +void KTTSD::slotTextAppended(const QCString& appId, const uint jobNum, const int partNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextAppended: Emitting DCOP signal textAppended with appId " << + appId << " job number " << jobNum << " part number " << partNum << endl; + textAppended(appId, jobNum, partNum); +} + +void KTTSD::slotTextRemoved(const QCString& appId, const uint jobNum){ + // Emit DCOP signal. + kdDebug() << "KTTSD::slotTextRemoved: Emitting DCOP signal textRemoved with appId " << appId << " job number " << jobNum << endl; + textRemoved(appId, jobNum); +} + +/** + * Returns the senderId (appId) of the DCOP application that called us. + * @return The DCOP sendId of calling application. + */ +const QCString KTTSD::getAppId() +{ + DCOPClient* client = callingDcopClient(); + QCString appId; + if (client) appId = client->senderId(); + return appId; +} + +/** +* If a job number is 0, returns the default job number for a command. +* Returns the job number of the last job queued by the application, or if +* no such job, the current job number. +* @return Default job number. 0 if no such job. +*/ +uint KTTSD::applyDefaultJobNum(const uint jobNum) +{ + uint jNum = jobNum; + if (!jNum) + { + jNum = m_speechData->findAJobNumByAppId(getAppId()); + if (!jNum) jNum = getCurrentTextJob(); + if (!jNum) jNum = m_speechData->findAJobNumByAppId(0); + } + return jNum; +} + +/* +* Fixes a string argument passed in via dcop. +* If NULL or "0" return QString::null. +*/ +QString KTTSD::fixNullString(const QString &talker) const +{ + if (!talker) return QString::null; + if (talker == "0") return QString::null; + return talker; +} + +// kspeech is obsolete. Applications should use KSpeech instead. + +// Constructor. +kspeech::kspeech(const QCString& objId, QObject *parent, const char *name) : + DCOPObject(objId), + QObject(parent, name), + m_kttsd("KSpeech") +{ +} + +// Destructor. +kspeech::~kspeech() { } + +// Delegate all DCOP methods to KTTSD object. +/*virtual*/ bool kspeech::supportsMarkup(const QString &talker, uint markupType) const + { return m_kttsd.supportsMarkup(talker, markupType); } +/*virtual*/ bool kspeech::supportsMarkers(const QString &talker) const + { return m_kttsd.supportsMarkers(talker); } +/*virtual*/ ASYNC kspeech::sayScreenReaderOutput(const QString &msg, const QString &talker) + { m_kttsd.sayScreenReaderOutput(msg, talker); } +/*virtual*/ ASYNC kspeech::sayWarning(const QString &warning, const QString &talker) + { m_kttsd.sayWarning(warning, talker); } +/*virtual*/ ASYNC kspeech::sayMessage(const QString &message, const QString &talker) + { m_kttsd.sayMessage(message, talker); } +/*virtual*/ ASYNC kspeech::setSentenceDelimiter(const QString &delimiter) + { m_kttsd.setSentenceDelimiter(delimiter); } +/*virtual*/ uint kspeech::setText(const QString &text, const QString &talker) + { return m_kttsd.setText(text, talker); } +/*virtual*/ uint kspeech::sayText(const QString &text, const QString &talker) + { return m_kttsd.sayText(text, talker); } +/*virtual*/ int kspeech::appendText(const QString &text, uint jobNum) + { return m_kttsd.appendText(text, jobNum); } +/*virtual*/ uint kspeech::setFile(const QString &filename, const QString &talker, + const QString& encoding) + { return m_kttsd.setFile(filename, talker, encoding); } +/*virtual*/ int kspeech::getTextCount(uint jobNum) + { return m_kttsd.getTextCount(jobNum); } +/*virtual*/ uint kspeech::getCurrentTextJob() + { return m_kttsd.getCurrentTextJob(); } +/*virtual*/ uint kspeech::getTextJobCount() + { return m_kttsd.getTextJobCount(); } +/*virtual*/ QString kspeech::getTextJobNumbers() + { return m_kttsd.getTextJobNumbers(); } +/*virtual*/ int kspeech::getTextJobState(uint jobNum) + { return m_kttsd.getTextJobState(jobNum); } +/*virtual*/ QByteArray kspeech::getTextJobInfo(uint jobNum) + { return m_kttsd.getTextJobInfo(jobNum); } +/*virtual*/ QString kspeech::talkerCodeToTalkerId(const QString& talkerCode) + { return m_kttsd.talkerCodeToTalkerId(talkerCode); } +/*virtual*/ QString kspeech::getTextJobSentence(uint jobNum, uint seq) + { return m_kttsd.getTextJobSentence(jobNum, seq); } +/*virtual*/ bool kspeech::isSpeakingText() const + { return m_kttsd.isSpeakingText(); } +/*virtual*/ ASYNC kspeech::removeText(uint jobNum) + { m_kttsd.removeText(jobNum); } +/*virtual*/ ASYNC kspeech::startText(uint jobNum) + { m_kttsd.startText(jobNum); } +/*virtual*/ ASYNC kspeech::stopText(uint jobNum) + { m_kttsd.stopText(jobNum); } +/*virtual*/ ASYNC kspeech::pauseText(uint jobNum) + { m_kttsd.pauseText(jobNum); } +/*virtual*/ ASYNC kspeech::resumeText(uint jobNum) + { m_kttsd.resumeText(jobNum); } +/*virtual*/ QStringList kspeech::getTalkers() + { return m_kttsd.getTalkers(); } +/*virtual*/ ASYNC kspeech::changeTextTalker(const QString &talker, uint jobNum ) + { m_kttsd.changeTextTalker(talker, jobNum); } +/*virtual*/ QString kspeech::userDefaultTalker() + { return m_kttsd.userDefaultTalker(); } +/*virtual*/ ASYNC kspeech::moveTextLater(uint jobNum) + { m_kttsd.moveTextLater(jobNum); } +/*virtual*/ int kspeech::jumpToTextPart(int partNum, uint jobNum) + { return m_kttsd.jumpToTextPart(partNum, jobNum); } +/*virtual*/ uint kspeech::moveRelTextSentence(int n, uint jobNum) + { return m_kttsd.moveRelTextSentence(n, jobNum); } +/*virtual*/ ASYNC kspeech::speakClipboard() + { m_kttsd.speakClipboard(); } +/*virtual*/ void kspeech::showDialog() + { m_kttsd.showDialog(); } +/*virtual*/ void kspeech::kttsdExit() + { m_kttsd.kttsdExit(); } +/*virtual*/ void kspeech::reinit() + { m_kttsd.reinit(); } +/*virtual*/ QString kspeech::version() + { return m_kttsd.version(); } + +#include "kttsd.moc" + diff --git a/kttsd/kttsd/kttsd.desktop b/kttsd/kttsd/kttsd.desktop new file mode 100644 index 0000000..27c216e --- /dev/null +++ b/kttsd/kttsd/kttsd.desktop @@ -0,0 +1,56 @@ +[Desktop Entry] +Type=Service +Exec=kttsd +Icon=kttsd +ServiceTypes=DCOP/Text-to-Speech +X-DCOP-ServiceType=Unique +X-DCOP-ServiceName=kttsd +X-KDE-StartupNotify=false +Name=KTTSD +Name[zh_TW]=KTTSd +Comment=KDE Text To Speech Daemon +Comment[bg]=Демон за управление на модула за синтез на глас +Comment[bs]=KDE Demon za izgovaranje teksta +Comment[ca]=Dimoni de text a veu de KDE +Comment[cs]=Démon hlasové syntézy KDE +Comment[da]=KDE's Tekst til tale-dæmon +Comment[de]=KDE Sprachausgabedienst +Comment[el]=KDE δαίμονας κειμένου-σε-ομιλία +Comment[es]=Demonio de KDE para la síntesis de texto a voz +Comment[et]=KDE teksti kõneks muutmise deemon +Comment[eu]=KDE-ren testutik hizketarako deabrua +Comment[fa]=شبح متن به گفتار KDE +Comment[fi]=KDE Teksti puheeksi -palvelinohjelma +Comment[fr]=Démon de synthèse vocale pour KDE +Comment[ga]=Deamhan Téacs-Go-Caint KDE +Comment[gl]=Servizo Texto-para-Fala de KDE +Comment[he]=שירות הטקסט לדיבור של KDE +Comment[hu]=KDE szövegfelolvasó szolgáltatás +Comment[is]=KDE texti-í-tal púki +Comment[it]=Demone di pronuncia di KDE +Comment[ja]=KDE テキスト読み上げデーモン +Comment[ka]=KDE ტექსტის გახმოვანების დემონი +Comment[km]=ដេមិនអត្ថបទដែលត្រូវនិយាយរបស់ KDE +Comment[mk]=Даемон на KDE за текст-во-говор +Comment[ms]=Daemon Teks Ke Tutur KDE +Comment[nb]=KDE tekst-til-tale-nisse +Comment[nds]=Vörlees-Dämoon vun KDE +Comment[ne]=केडीई पाठ वाचक डेइमन +Comment[nl]=KDE Tekst-tot-spraak-daemon +Comment[pa]=KDE ਪਾਠ ਤੋਂ ਬੋਲੀ ਡਾਈਮੋਨ +Comment[pl]=Usługa odczytywania tekstu dla KDE +Comment[pt]=Servidor do Texto para Fala do KDE +Comment[pt_BR]=Serviço de Conversão de Texto para Fala do KDE +Comment[ru]=Служба синтеза речи +Comment[sk]=Démon KDE text-na-reč +Comment[sl]=Demon KDE za besedilo v govor +Comment[sr]=KDE-ов демон за текст-у-говор +Comment[sr@Latn]=KDE-ov demon za tekst-u-govor +Comment[sv]=KDE:s text-till-tal demon +Comment[ta]=கேடியி உரையில் இருந்து பேச்சு டெமான் +Comment[tg]=Демон таҳлили овоз +Comment[tr]=KDE Metinden Konuşmaya Artalan Süreci +Comment[uk]=Демон KDE синтезу мовлення з тексту +Comment[vi]=Trình nền Văn bản sang Tiếng nói KDE +Comment[zh_TW]=KDE 文字轉語音的常駐精靈 + diff --git a/kttsd/kttsd/kttsd.h b/kttsd/kttsd/kttsd.h new file mode 100644 index 0000000..bdbd756 --- /dev/null +++ b/kttsd/kttsd/kttsd.h @@ -0,0 +1,686 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + KTTSD main class + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + ******************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; version 2 of the License. * + * * + ***************************************************************************/ + +#ifndef _KTTSD_H_ +#define _KTTSD_H_ + +#include "speechdata.h" +#include "talkermgr.h" +#include "speaker.h" +#include "kspeech.h" + +/** +* KTTSD - the KDE Text-to-speech Deamon. +* +* Provides the capability for applications to speak text. +* Applications may speak text by sending DCOP messages to application "kttsd" object "KSpeech". +* +* @author José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> +* @author Olaf Schmidt <ojschmidt@kde.org> +* @author Gary Cramblitt <garycramblitt@comcast.net> +*/ + +class KTTSD : public QObject, virtual public KSpeech +{ + Q_OBJECT + K_DCOP + + public: + /** + * Constructor. + * + * Create objects, speechData and speaker. + * Start thread + */ + KTTSD(const QCString& objId, QObject *parent=0, const char *name=0); + + /** + * Destructor. + * + * Terminate speaker thread. + */ + ~KTTSD(); + + /** DCOP exported functions for kspeech interface **/ + + /** + * Determine whether the currently-configured speech plugin supports a speech markup language. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * @param markupType The kttsd code for the desired speech markup language. + * @return True if the plugin currently configured for the indicated + * talker supports the indicated speech markup language. + * @see kttsdMarkupType + */ + virtual bool supportsMarkup(const QString &talker=NULL, const uint markupType = 0) const; + + /** + * Determine whether the currently-configured speech plugin supports markers in speech markup. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * @return True if the plugin currently configured for the indicated + * talker supports markers. + */ + virtual bool supportsMarkers(const QString &talker=NULL) const; + + /** + * Say a message as soon as possible, interrupting any other speech in progress. + * IMPORTANT: This method is reserved for use by Screen Readers and should not be used + * by any other applications. + * @param msg The message to be spoken. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + * + * If an existing Screen Reader output is in progress, it is stopped and discarded and + * replaced with this new message. + */ + virtual ASYNC sayScreenReaderOutput(const QString &msg, const QString &talker=NULL); + + /** + * Say a warning. The warning will be spoken when the current sentence + * stops speaking and takes precedence over Messages and regular text. Warnings should only + * be used for high-priority messages requiring immediate user attention, such as + * "WARNING. CPU is overheating." + * @param warning The warning to be spoken. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + */ + virtual ASYNC sayWarning(const QString &warning, const QString &talker=NULL); + + /** + * Say a message. The message will be spoken when the current sentence stops speaking + * but after any warnings have been spoken. + * Messages should be used for one-shot messages that can't wait for + * normal text messages to stop speaking, such as "You have mail.". + * @param message The message to be spoken. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * If no talker has been configured for the specified Talker code, + * defaults to the closest matching talker. + */ + virtual ASYNC sayMessage(const QString &message, const QString &talker=NULL); + + /** + * Sets the GREP pattern that will be used as the sentence delimiter. + * @param delimiter A valid GREP pattern. + * + * The default sentence delimiter is + @verbatim + ([\\.\\?\\!\\:\\;])\\s + @endverbatim + * + * Note that backward slashes must be escaped. + * + * Changing the sentence delimiter does not affect other applications. + * @see sentenceparsing + */ + virtual ASYNC setSentenceDelimiter(const QString &delimiter); + + /** + * Queue a text job. Does not start speaking the text. + * @param text The message to be spoken. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default plugin. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + * @return Job number. + * + * Plain text is parsed into individual sentences using the current sentence delimiter. + * Call @ref setSentenceDelimiter to change the sentence delimiter prior to calling setText. + * Call @ref getTextCount to retrieve the sentence count after calling setText. + * + * The text may contain speech mark language, such as Sable, JSML, or SMML, + * provided that the speech plugin/engine support it. In this case, + * sentence parsing follows the semantics of the markup language. + * + * Call @ref startText to mark the job as speakable and if the + * job is the first speakable job in the queue, speaking will begin. + * @see getTextCount + * @see startText + */ + virtual uint setText(const QString &text, const QString &talker=NULL); + + /** + * Say a plain text job. This is a convenience method that + * combines @ref setText and @ref startText into a single call. + * @param text The message to be spoken. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default plugin. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + * @return Job number. + * + * Plain text is parsed into individual sentences using the current sentence delimiter. + * Call @ref setSentenceDelimiter to change the sentence delimiter prior to + * calling setText. + * Call @ref getTextCount to retrieve the sentence count after calling setText. + * + * The text may contain speech mark language, such as Sable, JSML, or SSML, + * provided that the speech plugin/engine support it. In this case, + * sentence parsing follows the semantics of the markup language. + * + * The job is marked speakable. + * If there are other speakable jobs preceeding this one in the queue, + * those jobs continue speaking and when finished, this job will begin speaking. + * If there are no other speakable jobs preceeding this one, it begins speaking. + * + * @see getTextCount + * + * @since KDE 3.5 + */ + virtual uint sayText(const QString &text, const QString &talker); + + /** + * Adds another part to a text job. Does not start speaking the text. + * (thread safe) + * @param text The message to be spoken. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @return Part number for the added part. Parts are numbered starting at 1. + * + * The text is parsed into individual sentences. Call getTextCount to retrieve + * the sentence count. Call startText to mark the job as speakable and if the + * job is the first speakable job in the queue, speaking will begin. + * @see setText. + * @see startText. + */ + int appendText(const QString &text, const uint jobNum=0); + + /** + * Queue a text job from the contents of a file. Does not start speaking the text. + * @param filename Full path to the file to be spoken. May be a URL. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + * @param encoding Name of the encoding to use when reading the file. If + * NULL or Empty, uses default stream encoding. + * @return Job number. 0 if an error occurs. + * + * Plain text is parsed into individual sentences using the current sentence delimiter. + * Call @ref setSentenceDelimiter to change the sentence delimiter prior to calling setText. + * Call @ref getTextCount to retrieve the sentence count after calling setText. + * + * The text may contain speech mark language, such as Sable, JSML, or SMML, + * provided that the speech plugin/engine support it. In this case, + * sentence parsing follows the semantics of the markup language. + * + * Call @ref startText to mark the job as speakable and if the + * job is the first speakable job in the queue, speaking will begin. + * @see getTextCount + * @see startText + */ + virtual uint setFile(const QString &filename, const QString &talker=NULL, + const QString& encoding=NULL); + + /** + * Get the number of sentences in a text job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @return The number of sentences in the job. -1 if no such job. + * + * The sentences of a job are given sequence numbers from 1 to the number returned by this + * method. The sequence numbers are emitted in the @ref sentenceStarted and + * @ref sentenceFinished signals. + */ + virtual int getTextCount(const uint jobNum=0); + + /** + * Get the job number of the current text job. + * @return Job number of the current text job. 0 if no jobs. + * + * Note that the current job may not be speaking. See @ref isSpeakingText. + * @see getTextJobState. + * @see isSpeakingText + */ + virtual uint getCurrentTextJob(); + + /** + * Get the number of jobs in the text job queue. + * @return Number of text jobs in the queue. 0 if none. + */ + virtual uint getTextJobCount(); + + /** + * Get a comma-separated list of text job numbers in the queue. + * @return Comma-separated list of text job numbers in the queue. + */ + virtual QString getTextJobNumbers(); + + /** + * Get the state of a text job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @return State of the job. -1 if invalid job number. + * + * @see kttsdJobState + */ + virtual int getTextJobState(const uint jobNum=0); + + /** + * Get information about a text job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @return A QDataStream containing information about the job. + * Blank if no such job. + * + * The stream contains the following elements: + * - int state Job state. + * - QCString appId DCOP senderId of the application that requested the speech job. + * - QString talker Language code in which to speak the text. + * - int seq Current sentence being spoken. Sentences are numbered starting at 1. + * - int sentenceCount Total number of sentences in the job. + * - int partNum Current part of the job begin spoken. Parts are numbered starting at 1. + * - int partCount Total number of parts in the job. + * + * Note that sequence numbers apply to the entire job. They do not start from 1 at the beginning of + * each part. + * + * The following sample code will decode the stream: + @verbatim + QByteArray jobInfo = getTextJobInfo(jobNum); + QDataStream stream(jobInfo, IO_ReadOnly); + int state; + QCString appId; + QString talker; + int seq; + int sentenceCount; + int partNum; + int partCount; + stream >> state; + stream >> appId; + stream >> talker; + stream >> seq; + stream >> sentenceCount; + stream >> partNum; + stream >> partCount; + @endverbatim + */ + virtual QByteArray getTextJobInfo(const uint jobNum=0); + + /** + * Given a Talker Code, returns the Talker ID of the talker that would speak + * a text job with that Talker Code. + * @param talkerCode Talker Code. + * @return Talker ID of the talker that would speak the text job. + */ + virtual QString talkerCodeToTalkerId(const QString& talkerCode); + + /** + * Return a sentence of a job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @param seq Sequence number of the sentence. + * @return The specified sentence in the specified job. If not such + * job or sentence, returns "". + */ + virtual QString getTextJobSentence(const uint jobNum=0, const uint seq=1); + + /** + * Determine if kttsd is currently speaking any text jobs. + * @return True if currently speaking any text jobs. + */ + virtual bool isSpeakingText() const; + + /** + * Remove a text job from the queue. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * + * The job is deleted from the queue and the @ref textRemoved signal is emitted. + * + * If there is another job in the text queue, and it is marked speakable, + * that job begins speaking. + */ + virtual ASYNC removeText(const uint jobNum=0); + + /** + * Start a text job at the beginning. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * + * Rewinds the job to the beginning. + * + * The job is marked speakable. + * If there are other speakable jobs preceeding this one in the queue, + * those jobs continue speaking and when finished, this job will begin speaking. + * If there are no other speakable jobs preceeding this one, it begins speaking. + * + * The @ref textStarted signal is emitted when the text job begins speaking. + * When all the sentences of the job have been spoken, the job is marked for deletion from + * the text queue and the @ref textFinished signal is emitted. + */ + virtual ASYNC startText(const uint jobNum=0); + + /** + * Stop a text job and rewind to the beginning. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * + * The job is marked not speakable and will not be speakable until @ref startText or @ref resumeText + * is called. + * + * If there are speaking jobs preceeding this one in the queue, they continue speaking. + * If the job is currently speaking, the @ref textStopped signal is emitted and the job stops speaking. + * Depending upon the speech engine and plugin used, speeking may not stop immediately + * (it might finish the current sentence). + */ + virtual ASYNC stopText(const uint jobNum=0); + + /** + * Pause a text job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * + * The job is marked as paused and will not be speakable until @ref resumeText or + * @ref startText is called. + * + * If there are speaking jobs preceeding this one in the queue, they continue speaking. + * If the job is currently speaking, the @ref textPaused signal is emitted and the job stops speaking. + * Depending upon the speech engine and plugin used, speeking may not stop immediately + * (it might finish the current sentence). + * @see resumeText + */ + virtual ASYNC pauseText(const uint jobNum=0); + + /** + * Start or resume a text job where it was paused. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * + * The job is marked speakable. + * + * If the job is currently speaking, or is waiting to be spoken (speakable + * state), the resumeText() call is ignored. + * + * If the job is currently queued, or is finished, it is the same as calling + * @ref startText . + * + * If there are speaking jobs preceeding this one in the queue, those jobs continue speaking and, + * when finished this job will begin speaking where it left off. + * + * The @ref textResumed signal is emitted when the job resumes. + * @see pauseText + */ + virtual ASYNC resumeText(const uint jobNum=0); + + /** + * Get a list of the talkers configured in KTTS. + * @return A QStringList of fully-specified talker codes, one + * for each talker user has configured. + * + * @see talkers + */ + virtual QStringList getTalkers(); + + /** + * Change the talker for a text job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @param talker New code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + */ + virtual ASYNC changeTextTalker(const QString &talker, uint jobNum=0); + + /** + * Get the user's default talker. + * @return A fully-specified talker code. + * + * @see talkers + * @see getTalkers + */ + virtual QString userDefaultTalker(); + + /** + * Move a text job down in the queue so that it is spoken later. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * + * If the job is currently speaking, it is paused. + * If the next job in the queue is speakable, it begins speaking. + */ + virtual ASYNC moveTextLater(const uint jobNum=0); + + /** + * Jump to the first sentence of a specified part of a text job. + * @param partNum Part number of the part to jump to. Parts are numbered starting at 1. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @return Part number of the part actually jumped to. + * + * If partNum is greater than the number of parts in the job, jumps to last part. + * If partNum is 0, does nothing and returns the current part number. + * If no such job, does nothing and returns 0. + * Does not affect the current speaking/not-speaking state of the job. + */ + int jumpToTextPart(const int partNum, const uint jobNum=0); + + /** + * Advance or rewind N sentences in a text job. + * @param n Number of sentences to advance (positive) or rewind (negative) in the job. + * @param jobNum Job number of the text job. + * If zero, applies to the last job queued by the application, + * but if no such job, applies to the current job (if any). + * @return Sequence number of the sentence actually moved to. Sequence numbers + * are numbered starting at 1. + * + * If no such job, does nothing and returns 0. + * If n is zero, returns the current sequence number of the job. + * Does not affect the current speaking/not-speaking state of the job. + */ + uint moveRelTextSentence(const int n, const uint jobNum=0); + + /** + * Add the clipboard contents to the text queue and begin speaking it. + */ + virtual ASYNC speakClipboard(); + + /** + * Displays the %KTTS Manager dialog. In this dialog, the user may backup or skip forward in + * any text job by sentence or paragraph, rewind jobs, pause or resume jobs, or + * delete jobs. + */ + virtual void showDialog(); + + /** + * Stop the service. + */ + virtual void kttsdExit(); + + /** + * Re-start %KTTSD. + */ + virtual void reinit(); + + /** + * Return the KTTSD deamon version number. + * @since KDE 3.5 + */ + virtual QString version(); + + protected: + + k_dcop: + /** + * This signal is emitted by KNotify when a notification event occurs. + */ + void notificationSignal(const QString &event, const QString &fromApp, + const QString &text, const QString &sound, const QString &file, + const int present, const int level, const int winId, const int eventId ); + + private slots: + /* + * These functions are called whenever + * the status of the speaker object has changed + */ + void slotSentenceStarted(QString text, QString language, + const QCString& appId, const uint jobNum, const uint seq); + void slotSentenceFinished(const QCString& appId, const uint jobNum, const uint seq); + + /* + * These functions are called whenever + * the status of the speechData object has changed + */ + void slotTextSet(const QCString& appId, const uint jobNum); + void slotTextAppended(const QCString& appId, const uint jobNum, const int partNum); + void slotTextStarted(const QCString& appId, const uint jobNum); + void slotTextFinished(const QCString& appId, const uint jobNum); + void slotTextStopped(const QCString& appId, const uint jobNum); + void slotTextPaused(const QCString& appId, const uint jobNum); + void slotTextResumed(const QCString& appId, const uint jobNum); + void slotTextRemoved(const QCString& appId, const uint jobNum); + + /* + * Fires whenever user clicks Apply or OK buttons in Settings dialog. + */ + void configCommitted(); + + private: + /* + * Checks if KTTSD is ready to speak and at least one talker is configured. + * If not, user is prompted to display the configuration dialog. + */ + bool ready(); + + /* + * Create and initialize the SpeechData object. + */ + bool initializeSpeechData(); + + /* + * Create and initialize the TalkerMgr object. + */ + bool initializeTalkerMgr(); + + /* + * Create and initialize the speaker. + */ + bool initializeSpeaker(); + + /* + * Returns the senderId (appId) of the DCOP application that called us. + * @return appId The DCOP sendId of calling application. NULL if called internally by kttsd itself. + */ + const QCString getAppId(); + + /* + * If a job number is 0, returns the default job number for a command. + * Returns the job number of the last job queued by the application, or if + * no such job, the current job number. + * @return Default job number. 0 if no such job. + */ + uint applyDefaultJobNum(const uint jobNum); + + /* + * Fixes a talker argument passed in via dcop. + * If NULL or "0" return QString::null. + */ + QString fixNullString(const QString &talker) const; + + /* + * SpeechData containing all the data and the manipulating methods for all KTTSD + */ + SpeechData* m_speechData; + + /* + * TalkerMgr keeps a list of all the Talkers (synth plugins). + */ + TalkerMgr* m_talkerMgr; + + /* + * Speaker that will be run as another thread, actually saying the messages, warnings, and texts + */ + Speaker* m_speaker; +}; + +// kspeech is obsolete. Applications should use KSpeech instead. +class kspeech : public QObject, virtual public KSpeech +{ + Q_OBJECT + K_DCOP + + public: + // Constructor. + kspeech(const QCString& objId, QObject *parent=0, const char *name=0); + + // Destructor. + ~kspeech(); + + // Delegate all DCOP methods to KTTSD object. + virtual bool supportsMarkup(const QString &talker, uint markupType = 0) const; + virtual bool supportsMarkers(const QString &talker) const; + virtual ASYNC sayScreenReaderOutput(const QString &msg, const QString &talker); + virtual ASYNC sayWarning(const QString &warning, const QString &talker); + virtual ASYNC sayMessage(const QString &message, const QString &talker); + virtual ASYNC setSentenceDelimiter(const QString &delimiter); + virtual uint setText(const QString &text, const QString &talker); + virtual uint sayText(const QString &text, const QString &talker); + virtual int appendText(const QString &text, uint jobNum=0); + virtual uint setFile(const QString &filename, const QString &talker, + const QString& encoding); + virtual int getTextCount(uint jobNum=0); + virtual uint getCurrentTextJob(); + virtual uint getTextJobCount(); + virtual QString getTextJobNumbers(); + virtual int getTextJobState(uint jobNum=0); + virtual QByteArray getTextJobInfo(uint jobNum=0); + virtual QString talkerCodeToTalkerId(const QString& talkerCode); + virtual QString getTextJobSentence(uint jobNum=0, uint seq=0); + virtual bool isSpeakingText() const; + virtual ASYNC removeText(uint jobNum=0); + virtual ASYNC startText(uint jobNum=0); + virtual ASYNC stopText(uint jobNum=0); + virtual ASYNC pauseText(uint jobNum=0); + virtual ASYNC resumeText(uint jobNum=0); + virtual QStringList getTalkers(); + virtual ASYNC changeTextTalker(const QString &talker, uint jobNum=0 ); + virtual QString userDefaultTalker(); + virtual ASYNC moveTextLater(uint jobNum=0); + virtual int jumpToTextPart(int partNum, uint jobNum=0); + virtual uint moveRelTextSentence(int n, uint jobNum=0); + virtual ASYNC speakClipboard(); + virtual void showDialog(); + virtual void kttsdExit(); + virtual void reinit(); + virtual QString version(); + + private: + KTTSD m_kttsd; +}; + +#endif // _KTTSD_H_ diff --git a/kttsd/kttsd/main.cpp b/kttsd/kttsd/main.cpp new file mode 100644 index 0000000..3c47df4 --- /dev/null +++ b/kttsd/kttsd/main.cpp @@ -0,0 +1,68 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Where the main function for KTTSD resides. + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#include <kuniqueapplication.h> +#include <kaboutdata.h> +#include <kcmdlineargs.h> +#include <kdebug.h> +#include <klocale.h> +#include <dcopclient.h> + +#include "kttsd.h" + +int main (int argc, char *argv[]){ + KLocale::setMainCatalogue("kttsd"); + KAboutData aboutdata("kttsd", I18N_NOOP("kttsd"), + "0.3.5.2", I18N_NOOP("Text-to-speech synthesis deamon"), + KAboutData::License_GPL, "(C) 2002, José Pablo Ezequiel Fernández"); + aboutdata.addAuthor("José Pablo Ezequiel Fernández",I18N_NOOP("Original Author"),"pupeno@pupeno.com"); + aboutdata.addAuthor("Gary Cramblitt", I18N_NOOP("Maintainer"),"garycramblitt@comcast.net"); + aboutdata.addAuthor("Gunnar Schmi Dt", I18N_NOOP("Contributor"),"gunnar@schmi-dt.de"); + aboutdata.addAuthor("Olaf Schmidt", I18N_NOOP("Contributor"),"ojschmidt@kde.org"); + aboutdata.addAuthor("Paul Giannaros", I18N_NOOP("Contributor"), "ceruleanblaze@gmail.com"); + aboutdata.addCredit("Jorge Luis Arzola", I18N_NOOP("Testing"), "arzolacub@hotmail.com"); + aboutdata.addCredit("David Powell", I18N_NOOP("Testing"), "achiestdragon@gmail.com"); + + KCmdLineArgs::init( argc, argv, &aboutdata ); + // KCmdLineArgs::addCmdLineOptions( options ); + KUniqueApplication::addCmdLineOptions(); + + if(!KUniqueApplication::start()){ + kdDebug() << "KTTSD is already running" << endl; + return (0); + } + + KUniqueApplication app; + // This app is started automatically, no need for session management + app.disableSessionManagement(); + // TODO: kspeech is obsolete. Use KSpeech instead. For backwards compatibility, + // kspeech creates the "real" KSpeech object (KTTSD). At some point in the future, + // change following statement to + // KTTSD *service = new KTTSD("KSpeech"); + kspeech *service = new kspeech("kspeech"); + + // kdDebug() << "Entering event loop." << endl; + return app.exec(); + delete service; +} diff --git a/kttsd/kttsd/speaker.cpp b/kttsd/kttsd/speaker.cpp new file mode 100644 index 0000000..b965a9e --- /dev/null +++ b/kttsd/kttsd/speaker.cpp @@ -0,0 +1,1701 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Speaker class. + This class is in charge of getting the messages, warnings and text from + the queue and call the plug ins function to actually speak the texts. + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + ******************************************************************************/ + +/****************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License. * + * * + ******************************************************************************/ + +// Qt includes. +#include <qfile.h> +#include <qtimer.h> +#include <qdir.h> + +// KDE includes. +#include <kdebug.h> +#include <klocale.h> +#include <kparts/componentfactory.h> +#include <ktrader.h> +#include <kapplication.h> +#include <kstandarddirs.h> +#include <ktempfile.h> +//#include <kio/job.h> + +// KTTSD includes. +#include "player.h" +#include "speaker.h" +#include "speaker.moc" +#include "talkermgr.h" +#include "utils.h" + +/** +* The Speaker class takes sentences from the text queue, messages from the +* messages queue, warnings from the warnings queue, and Screen Reader +* output and places them into an internal "utterance queue". It then +* loops through this queue, farming the work off to the plugins. +* It tries to optimize processing so as to keep the plugins busy as +* much as possible, while ensuring that only one stream of audio is +* heard at any one time. +* +* The message queues are maintained in the SpeechData class. +* +* Text jobs in the text queue each have a state (queued, speakable, +* speaking, paused, finished). Each plugin has a state (idle, saying, synthing, +* or finished). And finally, each utterance has a state (waiting, saying, +* synthing, playing, finished). It can be confusing if you are not aware +* of all these states. +* +* Speaker takes some pains to ensure speech is spoken in the correct order, +* namely Screen Reader Output has the highest priority, Warnings are next, +* Messages are next, and finally regular text jobs. Since Screen Reader +* Output, Warnings, and Messages can be queued in the middle of a text +* job, Speaker must be prepared to reorder utterances in its queue. +* +* At the same time, it must issue the signals to inform programs +* what is happening. +* +* Finally, users can pause, restart, delete, advance, or rewind text jobs +* and Speaker must respond to these commands. In some cases, utterances that +* have already been synthesized and are ready for audio output must be +* discarded in response to these commands. +* +* Some general guidelines for programmers modifying this code: +* - Avoid blocking at all cost. If a plugin won't stopText, keep going. +* You might have to wait for the plugin to complete before asking it +* to perform the next operation, but in the meantime, there might be +* other useful work that can be performed. +* - In no case allow the main thread Qt event loop to block. +* - Plugins that do not have asynchronous support are wrapped in the +* ThreadedPlugin class, which attempts to make them as asynchronous as +* it can, but there are limits. +* - doUtterances is the main worker method. If in doubt, call doUtterances. +* - Because Speaker controls the ordering of utterances, most sequence-related +* signals must be emitted by Speaker; not SpeechData. Only the add +* and delete job-related signals eminate from SpeechData. +* - The states of the 3 types of objects mentioned above (jobs, utterances, +* and plugins) can interact in subtle ways. Test fully. For example, while +* a text job might be in a paused state, the plugin could be synthesizing +* in anticipation of resume, or sythesizing a Warning, Message, or +* Screen Reader Output. Meanwhile, while one of the utterances might +* have a paused state, others from the same job could be synthing, waiting, +* or finished. +* - There can be more than one Audio Player object in existence at one time, although +* there must never be more than one actually playing at one time. For +* example, an Audio Player playing an utterance from a text job can be +* in a paused state, while another Audio Player is playing a Screen Reader +* Output utterance. Finally, since some plugins do their own audio, it +* might be that none of the Audio Player objects are playing. +*/ + +/* Public Methods ==========================================================*/ + +/** +* Constructor. +* Loads plugins. +*/ +Speaker::Speaker( SpeechData*speechData, TalkerMgr* talkerMgr, + QObject *parent, const char *name) : + QObject(parent, name), + m_speechData(speechData), + m_talkerMgr(talkerMgr) +{ + // kdDebug() << "Running: Speaker::Speaker()" << endl; + m_exitRequested = false; + m_textInterrupted = false; + m_currentJobNum = 0; + m_lastAppId = 0; + m_lastJobNum = 0; + m_lastSeq = 0; + m_timer = new QTimer(this, "kttsdAudioTimer"); + m_speechData->config->setGroup("General"); + m_playerOption = m_speechData->config->readNumEntry("AudioOutputMethod", 0); // default to aRts. + // Map 50% to 100% onto 2.0 to 0.5. + m_audioStretchFactor = 1.0/(float(m_speechData->config->readNumEntry("AudioStretchFactor", 100))/100.0); + switch (m_playerOption) + { + case 0: break; + case 1: + m_speechData->config->setGroup("GStreamerPlayer"); + m_sinkName = m_speechData->config->readEntry("SinkName", "osssink"); + m_periodSize = m_speechData->config->readNumEntry("PeriodSize", 128); + m_periods = m_speechData->config->readNumEntry("Periods", 8); + m_playerDebugLevel = m_speechData->config->readNumEntry("DebugLevel", 1); + break; + case 2: + m_speechData->config->setGroup("ALSAPlayer"); + m_sinkName = m_speechData->config->readEntry("PcmName", "default"); + if ("custom" == m_sinkName) + m_sinkName = m_speechData->config->readEntry("CustomPcmName", "default"); + m_periodSize = m_speechData->config->readNumEntry("PeriodSize", 128); + m_periods = m_speechData->config->readNumEntry("Periods", 8); + m_playerDebugLevel = m_speechData->config->readNumEntry("DebugLevel", 1); + break; + case 3: + m_speechData->config->setGroup("aKodePlayer"); + m_sinkName = m_speechData->config->readEntry("SinkName", "auto"); + m_periodSize = m_speechData->config->readNumEntry("PeriodSize", 128); + m_periods = m_speechData->config->readNumEntry("Periods", 8); + m_playerDebugLevel = m_speechData->config->readNumEntry("DebugLevel", 1); + break; + } + // Connect timer timeout signal. + connect(m_timer, SIGNAL(timeout()), this, SLOT(slotTimeout())); + + // Connect plugins to slots. + QPtrList<PlugInProc> plugins = m_talkerMgr->getLoadedPlugIns(); + const int pluginsCount = plugins.count(); + for (int ndx = 0; ndx < pluginsCount; ++ndx) + { + PlugInProc* speech = plugins.at(ndx); + connect(speech, SIGNAL(synthFinished()), + this, SLOT(slotSynthFinished())); + connect(speech, SIGNAL(sayFinished()), + this, SLOT(slotSayFinished())); + connect(speech, SIGNAL(stopped()), + this, SLOT(slotStopped())); + connect(speech, SIGNAL(error(bool, const QString&)), + this, SLOT(slotError(bool, const QString&))); + } +} + +/** +* Destructor. +*/ +Speaker::~Speaker(){ + // kdDebug() << "Running: Speaker::~Speaker()" << endl; + m_timer->stop(); + delete m_timer; + if (!m_uttQueue.isEmpty()) + { + uttIterator it; + for (it = m_uttQueue.begin(); it != m_uttQueue.end(); ) + it = deleteUtterance(it); + } +} + +/** + * Tells the speaker it is requested to exit. + * TODO: I don't think this actually accomplishes anything. + */ +void Speaker::requestExit(){ + // kdDebug() << "Speaker::requestExit: Running" << endl; + m_exitRequested = true; +} + +/** + * Main processing loop. Dequeues utterances and sends them to the + * plugins and/or Audio Player. + */ +void Speaker::doUtterances() +{ + // kdDebug() << "Running: Speaker::doUtterances()" << endl; + + // Used to prevent exiting prematurely. + m_again = true; + + while(m_again && !m_exitRequested) + { + m_again = false; + + if (m_exitRequested) + { + // kdDebug() << "Speaker::run: exiting due to request 1." << endl; + return; + } + + uttIterator it; + uttIterator itBegin; + uttIterator itEnd = 0; // Init to zero to avoid compiler warning. + + // If Screen Reader Output is waiting, we need to process it ASAP. + if (m_speechData->screenReaderOutputReady()) + { + m_again = getNextUtterance(); + } +// kdDebug() << "Speaker::doUtterances: queue dump:" << endl; +// for (it = m_uttQueue.begin(); it != m_uttQueue.end(); ++it) +// { +// QString pluginState = "no plugin"; +// if (it->plugin) pluginState = pluginStateToStr(it->plugin->getState()); +// QString jobState = +// jobStateToStr(m_speechData->getTextJobState(it->sentence->jobNum)); +// kdDebug() << +// " State: " << uttStateToStr(it->state) << +// "," << pluginState << +// "," << jobState << +// " Type: " << uttTypeToStr(it->utType) << +// " Text: " << it->sentence->text << endl; +// } + + if (!m_uttQueue.isEmpty()) + { + // Delete utterances that are finished. + it = m_uttQueue.begin(); + while (it != m_uttQueue.end()) + { + if (it->state == usFinished) + it = deleteUtterance(it); + else + ++it; + } + // Loop through utterance queue. + int waitingCnt = 0; + int waitingMsgCnt = 0; + int transformingCnt = 0; + bool playing = false; + int synthingCnt = 0; + itEnd = m_uttQueue.end(); + itBegin = m_uttQueue.begin(); + for (it = itBegin; it != itEnd; ++it) + { + uttState utState = it->state; + uttType utType = it->utType; + switch (utState) + { + case usNone: + { + setInitialUtteranceState(*it); + m_again = true; + break; + } + case usWaitingTransform: + { + // Create an XSLT transformer and transform the text. + it->transformer = new SSMLConvert(); + connect(it->transformer, SIGNAL(transformFinished()), + this, SLOT(slotTransformFinished())); + if (it->transformer->transform(it->sentence->text, + it->plugin->getSsmlXsltFilename())) + { + it->state = usTransforming; + ++transformingCnt; + } + else + { + // If an error occurs transforming, skip it. + it->state = usTransforming; + setInitialUtteranceState(*it); + } + m_again = true; + break; + } + case usTransforming: + { + // See if transformer is finished. + if (it->transformer->getState() == SSMLConvert::tsFinished) + { + // Get the transformed text. + it->sentence->text = it->transformer->getOutput(); + // Set next state (usWaitingSynth or usWaitingSay) + setInitialUtteranceState(*it); + m_again = true; + --transformingCnt; + } + break; + } + case usWaitingSignal: + { + // If first in queue, emit signal. + if (it == itBegin) + { + if (utType == utStartOfJob) + { + m_speechData->setTextJobState( + it->sentence->jobNum, KSpeech::jsSpeaking); + if (it->sentence->seq == 0) + emit textStarted(it->sentence->appId, + it->sentence->jobNum); + else + emit textResumed(it->sentence->appId, + it->sentence->jobNum); + } else { + m_speechData->setTextJobState( + it->sentence->jobNum, KSpeech::jsFinished); + emit textFinished(it->sentence->appId, it->sentence->jobNum); + } + it->state = usFinished; + m_again = true; + } + break; + } + case usSynthed: + { + // Don't bother stretching if factor is 1.0. + // Don't bother stretching if SSML. + // TODO: This is because sox mangles SSML pitch settings. Would be nice + // to figure out how to avoid this. + if (m_audioStretchFactor == 1.0 || it->isSSML) + { + it->state = usStretched; + m_again = true; + } + else + { + it->audioStretcher = new Stretcher(); + connect(it->audioStretcher, SIGNAL(stretchFinished()), + this, SLOT(slotStretchFinished())); + if (it->audioStretcher->stretch(it->audioUrl, makeSuggestedFilename(), + m_audioStretchFactor)) + { + it->state = usStretching; + m_again = true; // Is this needed? + } + else + { + // If stretch failed, it is most likely caused by sox not being + // installed. Just skip it. + it->state = usStretched; + m_again = true; + delete it->audioStretcher; + it->audioStretcher= 0; + } + } + break; + } + case usStretching: + { + // See if Stretcher is finished. + if (it->audioStretcher->getState() == Stretcher::ssFinished) + { + QFile::remove(it->audioUrl); + it->audioUrl = it->audioStretcher->getOutFilename(); + it->state = usStretched; + delete it->audioStretcher; + it->audioStretcher = 0; + m_again = true; + } + break; + } + case usStretched: + { + // If first in queue, start playback. + if (it == itBegin) + { + if (startPlayingUtterance(it)) + { + playing = true; + m_again = true; + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + break; + } + case usPlaying: + { + playing = true; + break; + } + case usPaused: + case usPreempted: + { + if (!playing) + { + if (startPlayingUtterance(it)) + { + playing = true; + m_again = true; + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + break; + } + case usWaitingSay: + { + // If first in queue, start it. + if (it == itBegin) + { + int jobState = + m_speechData->getTextJobState(it->sentence->jobNum); + if ((jobState == KSpeech::jsSpeaking) || + (jobState == KSpeech::jsSpeakable)) + { + if (it->plugin->getState() == psIdle) + { + // Set job to speaking state and set sequence number. + mlText* sentence = it->sentence; + m_currentJobNum = sentence->jobNum; + m_speechData->setTextJobState(m_currentJobNum, KSpeech::jsSpeaking); + m_speechData->setJobSequenceNum(m_currentJobNum, sentence->seq); + prePlaySignals(it); + // kdDebug() << "Async synthesis and audibilizing." << endl; + it->state = usSaying; + playing = true; + it->plugin->sayText(it->sentence->text); + m_again = true; + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + } else { + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + break; + } + case usWaitingSynth: + { + // TODO: If the synth is busy and the waiting text is screen + // reader output, it would be nice to call the synth's + // stopText() method. However, some of the current plugins + // have horrible startup times, so we won't do that for now. + if (it->plugin->getState() == psIdle) + { + // kdDebug() << "Async synthesis." << endl; + it->state = usSynthing; + ++synthingCnt; + it->plugin->synthText(it->sentence->text, + makeSuggestedFilename()); + m_again = true; + } + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + break; + } + case usSaying: + { + // See if synthesis and audibilizing is finished. + if (it->plugin->getState() == psFinished) + { + it->plugin->ackFinished(); + it->state = usFinished; + m_again = true; + } else { + playing = true; + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + } + break; + } + case usSynthing: + { + // See if synthesis is completed. + if (it->plugin->getState() == psFinished) + { + it->audioUrl = KStandardDirs::realFilePath(it->plugin->getFilename()); + // kdDebug() << "Speaker::doUtterances: synthesized filename: " << it->audioUrl << endl; + it->plugin->ackFinished(); + it->state = usSynthed; + m_again = true; + } else ++synthingCnt; + ++waitingCnt; + if (utType == utWarning || utType == utMessage) ++waitingMsgCnt; + break; + } + case usFinished: break; + } + } + // See if there are any messages or warnings to process. + // We keep up to 2 such utterances in the queue. + if ((waitingMsgCnt < 2) && (transformingCnt < 3)) + { + if (m_speechData->warningInQueue() || m_speechData->messageInQueue()) + { + getNextUtterance(); + m_again = true; + } + } + // Try to keep at least two utterances in the queue waiting to be played, + // and no more than 3 transforming at one time. + if ((waitingCnt < 2) && (transformingCnt < 3)) + if (getNextUtterance()) m_again = true; + } else { + // See if another utterance is ready to be worked on. + // If so, loop again since we've got work to do. + m_again = getNextUtterance(); + } + } + // kdDebug() << "Speaker::doUtterances: exiting." << endl; +} + +/** + * Determine if kttsd is currently speaking any text jobs. + * @return True if currently speaking any text jobs. + */ +bool Speaker::isSpeakingText() +{ + return (m_speechData->getTextJobState(m_currentJobNum) == KSpeech::jsSpeaking); +} + +/** + * Get the job number of the current text job. + * @return Job number of the current text job. 0 if no jobs. + * + * Note that the current job may not be speaking. See @ref isSpeakingText. + */ +uint Speaker::getCurrentTextJob() { return m_currentJobNum; } + +/** + * Remove a text job from the queue. + * @param jobNum Job number of the text job. + * + * The job is deleted from the queue and the @ref textRemoved signal is emitted. + * + * If there is another job in the text queue, and it is marked speakable, + * that job begins speaking. + */ +void Speaker::removeText(const uint jobNum) +{ + deleteUtteranceByJobNum(jobNum); + m_speechData->removeText(jobNum); + doUtterances(); +} + +/** + * Start a text job at the beginning. + * @param jobNum Job number of the text job. + * + * Rewinds the job to the beginning. + * + * The job is marked speakable. + * If there are other speakable jobs preceeding this one in the queue, + * those jobs continue speaking and when finished, this job will begin speaking. + * If there are no other speakable jobs preceeding this one, it begins speaking. + * + * The @ref textStarted signal is emitted when the text job begins speaking. + * When all the sentences of the job have been spoken, the job is marked for deletion from + * the text queue and the @ref textFinished signal is emitted. + */ +void Speaker::startText(const uint jobNum) +{ + deleteUtteranceByJobNum(jobNum); + m_speechData->setJobSequenceNum(jobNum, 1); + m_speechData->setTextJobState(jobNum, KSpeech::jsSpeakable); + if (m_lastJobNum == jobNum) + { + // kdDebug() << "Speaker::startText: startText called on speaking job " << jobNum << endl; + m_lastJobNum = 0; + m_lastAppId = 0; + m_lastSeq = 0; + } + doUtterances(); +} + +/** + * Stop a text job and rewind to the beginning. + * @param jobNum Job number of the text job. + * + * The job is marked not speakable and will not be speakable until @ref startText or @ref resumeText + * is called. + * + * If there are speaking jobs preceeding this one in the queue, they continue speaking. + * If the job is currently speaking, the @ref textStopped signal is emitted and the job stops speaking. + * Depending upon the speech engine and plugin used, speeking may not stop immediately + * (it might finish the current sentence). + */ +void Speaker::stopText(const uint jobNum) +{ + bool emitSignal = (m_speechData->getTextJobState(jobNum) == KSpeech::jsSpeaking); + deleteUtteranceByJobNum(jobNum); + m_speechData->setJobSequenceNum(jobNum, 1); + m_speechData->setTextJobState(jobNum, KSpeech::jsQueued); + if (emitSignal) textStopped(m_speechData->getAppIdByJobNum(jobNum), jobNum); + // Call doUtterances to process other jobs. + doUtterances(); +} + +/** + * Pause a text job. + * @param jobNum Job number of the text job. + * + * The job is marked as paused and will not be speakable until @ref resumeText or + * @ref startText is called. + * + * If there are speaking jobs preceeding this one in the queue, they continue speaking. + * If the job is currently speaking, the @ref textPaused signal is emitted and the job stops speaking. + * Depending upon the speech engine and plugin used, speeking may not stop immediately + * (it might finish the current sentence). + * @see resumeText + */ +void Speaker::pauseText(const uint jobNum) +{ + bool emitSignal = (m_speechData->getTextJobState(jobNum) == KSpeech::jsSpeaking); + pauseUtteranceByJobNum(jobNum); + kdDebug() << "Speaker::pauseText: setting Job State of job " << jobNum << " to jsPaused" << endl; + m_speechData->setTextJobState(jobNum, KSpeech::jsPaused); + if (emitSignal) textPaused(m_speechData->getAppIdByJobNum(jobNum),jobNum); +} + +/** + * Start or resume a text job where it was paused. + * @param jobNum Job number of the text job. + * + * The job is marked speakable. + * + * If the job is currently speaking, or is waiting to be spoken (speakable + * state), the resumeText() call is ignored. + * + * If the job is currently queued, or is finished, it is the same as calling + * @ref startText . + * + * If there are speaking jobs preceeding this one in the queue, those jobs continue speaking and, + * when finished this job will begin speaking where it left off. + * + * The @ref textResumed signal is emitted when the job resumes. + * @see pauseText + */ +void Speaker::resumeText(const uint jobNum) +{ + int state = m_speechData->getTextJobState(jobNum); + switch (state) + { + case KSpeech::jsQueued: + case KSpeech::jsFinished: + startText(jobNum); + break; + case KSpeech::jsSpeakable: + case KSpeech::jsSpeaking: + doUtterances(); + break; + case KSpeech::jsPaused: + if (jobNum == m_currentJobNum) + m_speechData->setTextJobState(jobNum, KSpeech::jsSpeaking); + else + m_speechData->setTextJobState(jobNum, KSpeech::jsSpeakable); + doUtterances(); + break; + } +} + +/** + * Move a text job down in the queue so that it is spoken later. + * @param jobNum Job number of the text job. + * + * If the job is currently speaking, it is paused. + * If the next job in the queue is speakable, it begins speaking. + */ +void Speaker::moveTextLater(const uint jobNum) +{ + if (m_speechData->getTextJobState(jobNum) == KSpeech::jsSpeaking) + m_speechData->setTextJobState(jobNum, KSpeech::jsPaused); + deleteUtteranceByJobNum(jobNum); + m_speechData->moveTextLater(jobNum); + doUtterances(); +} + +/** + * Jump to the first sentence of a specified part of a text job. + * @param partNum Part number of the part to jump to. Parts are numbered starting at 1. + * @param jobNum Job number of the text job. + * @return Part number of the part actually jumped to. + * + * If partNum is greater than the number of parts in the job, jumps to last part. + * If partNum is 0, does nothing and returns the current part number. + * If no such job, does nothing and returns 0. + * Does not affect the current speaking/not-speaking state of the job. + */ +int Speaker::jumpToTextPart(const int partNum, const uint jobNum) +{ + if (partNum == 0) return m_speechData->jumpToTextPart(partNum, jobNum); + deleteUtteranceByJobNum(jobNum); + int pNum = m_speechData->jumpToTextPart(partNum, jobNum); + if (pNum) + { + uint seq = m_speechData->getJobSequenceNum(jobNum); + if (jobNum == m_lastJobNum) + { + if (seq == 0) + m_lastSeq = seq; + else + m_lastSeq = seq - 1; + } + if (jobNum == m_currentJobNum) + { + m_lastJobNum = jobNum; + if (seq == 0) + m_lastSeq = 0; + else + m_lastSeq = seq - 1; + doUtterances(); + } + } + return pNum; +} + +/** + * Advance or rewind N sentences in a text job. + * @param n Number of sentences to advance (positive) or rewind (negative) + * in the job. + * @param jobNum Job number of the text job. + * @return Sequence number of the sentence actually moved to. + * Sequence numbers are numbered starting at 1. + * + * If no such job, does nothing and returns 0. + * If n is zero, returns the current sequence number of the job. + * Does not affect the current speaking/not-speaking state of the job. + */ +uint Speaker::moveRelTextSentence(const int n, const uint jobNum) +{ + if (0 == n) + return m_speechData->getJobSequenceNum(jobNum); + else { + deleteUtteranceByJobNum(jobNum); + // TODO: More efficient way to advance one or two sentences, since there is a + // good chance those utterances are already in the queue and synthesized. + uint seq = m_speechData->moveRelTextSentence(n, jobNum); + kdDebug() << "Speaker::moveRelTextSentence: job num: " << jobNum << " moved to seq: " << seq << endl; + if (jobNum == m_lastJobNum) + { + if (seq == 0) + m_lastSeq = seq; + else + m_lastSeq = seq - 1; + } + if (jobNum == m_currentJobNum) + { + m_lastJobNum = jobNum; + if (seq == 0) + m_lastSeq = 0; + else + m_lastSeq = seq - 1; + doUtterances(); + } + return seq; + } +} + +/* Private Methods ==========================================================*/ + +/** + * Converts an utterance state enumerator to a displayable string. + * @param state Utterance state. + */ +QString Speaker::uttStateToStr(uttState state) +{ + switch (state) + { + case usNone: return "usNone"; + case usWaitingTransform: return "usWaitingTransform"; + case usTransforming: return "usTransforming"; + case usWaitingSay: return "usWaitingSay"; + case usWaitingSynth: return "usWaitingSynth"; + case usWaitingSignal: return "usWaitingSignal"; + case usSaying: return "usSaying"; + case usSynthing: return "usSynthing"; + case usSynthed: return "usSynthed"; + case usStretching: return "usStretching"; + case usStretched: return "usStretched"; + case usPlaying: return "usPlaying"; + case usPaused: return "usPaused"; + case usPreempted: return "usPreempted"; + case usFinished: return "usFinished"; + } + return QString::null; +} + +/** + * Converts an utterance type enumerator to a displayable string. + * @param utType Utterance type. + * @return Displayable string for utterance type. + */ +QString Speaker::uttTypeToStr(uttType utType) +{ + switch (utType) + { + case utText: return "utText"; + case utInterruptMsg: return "utInterruptMsg"; + case utInterruptSnd: return "utInterruptSnd"; + case utResumeMsg: return "utResumeMsg"; + case utResumeSnd: return "utResumeSnd"; + case utMessage: return "utMessage"; + case utWarning: return "utWarning"; + case utScreenReader: return "utScreenReader"; + case utStartOfJob: return "utStartOfJob"; + case utEndOfJob: return "utEndOfJob"; + } + return QString::null; +} + +/** + * Converts a plugin state enumerator to a displayable string. + * @param state Plugin state. + * @return Displayable string for plugin state. + */ +QString Speaker::pluginStateToStr(pluginState state) +{ + switch( state ) + { + case psIdle: return "psIdle"; + case psSaying: return "psSaying"; + case psSynthing: return "psSynthing"; + case psFinished: return "psFinished"; + } + return QString::null; +} + +/** + * Converts a job state enumerator to a displayable string. + * @param state Job state. + * @return Displayable string for job state. + */ +QString Speaker::jobStateToStr(int state) +{ + switch ( state ) + { + case KSpeech::jsQueued: return "jsQueued"; + case KSpeech::jsSpeakable: return "jsSpeakable"; + case KSpeech::jsSpeaking: return "jsSpeaking"; + case KSpeech::jsPaused: return "jsPaused"; + case KSpeech::jsFinished: return "jsFinished"; + } + return QString::null; +} + +/** + * Delete any utterances in the queue with this jobNum. + * @param jobNum Job Number of the utterances to delete. + * If currently processing any deleted utterances, stop them. + */ +void Speaker::deleteUtteranceByJobNum(const uint jobNum) +{ + uttIterator it = m_uttQueue.begin(); + while (it != m_uttQueue.end()) + { + if (it->sentence) + { + if (it->sentence->jobNum == jobNum) + it = deleteUtterance(it); + else + ++it; + } else + ++it; + } +} + +/** + * Pause the utterance with this jobNum if it is playing on the Audio Player. + * @param jobNum The Job Number of the utterance to pause. + */ +void Speaker::pauseUtteranceByJobNum(const uint jobNum) +{ + uttIterator itEnd = m_uttQueue.end(); + for (uttIterator it = m_uttQueue.begin(); it != itEnd; ++it) + { + if (it->sentence) // TODO: Why is this necessary? + if (it->sentence->jobNum == jobNum) + if (it->state == usPlaying) + { + if (it->audioPlayer) + if (it->audioPlayer->playing()) + { + m_timer->stop(); + kdDebug() << "Speaker::pauseUtteranceByJobNum: pausing audio player" << endl; + it->audioPlayer->pause(); + kdDebug() << "Speaker::pauseUtteranceByJobNum: Setting utterance state to usPaused" << endl; + it->state = usPaused; + return; + } + // Audio player has finished, but timeout hasn't had a chance + // to clean up. So do nothing, and let timeout do the cleanup. + } + } +} + +/** + * Determines whether the given text is SSML markup. + */ +bool Speaker::isSsml(const QString &text) +{ + return KttsUtils::hasRootElement( text, "speak" ); +} + +/** + * Determines the initial state of an utterance. If the utterance contains + * SSML, the state is set to usWaitingTransform. Otherwise, if the plugin + * supports async synthesis, sets to usWaitingSynth, otherwise usWaitingSay. + * If an utterance has already been transformed, usWaitingTransform is + * skipped to either usWaitingSynth or usWaitingSay. + * @param utt The utterance. + */ +void Speaker::setInitialUtteranceState(Utt &utt) +{ + if ((utt.state != usTransforming) && utt.isSSML) +{ + utt.state = usWaitingTransform; + return; +} + if (utt.plugin->supportsSynth()) + utt.state = usWaitingSynth; + else + utt.state = usWaitingSay; +} + +/** + * Returns true if the given job and sequence number are already in the utterance queue. + */ +bool Speaker::isInUtteranceQueue(uint jobNum, uint seqNum) +{ + uttIterator itEnd = m_uttQueue.end(); + for (uttIterator it = m_uttQueue.begin(); it != itEnd; ++it) + { + if (it->sentence) + { + if (it->sentence->jobNum == jobNum && it->sentence->seq == seqNum) return true; + } + } + return false; +} + +/** + * Gets the next utterance to be spoken from speechdata and adds it to the queue. + * @return True if one or more utterances were added to the queue. + * + * Checks for waiting ScreenReaderOutput, Warnings, Messages, or Text, + * in that order. + * If Warning or Message and interruption messages have been configured, + * adds those to the queue as well. + * Determines which plugin should be used for the utterance. + */ +bool Speaker::getNextUtterance() +{ + bool gotOne = false; + Utt* utt = 0; + if (m_speechData->screenReaderOutputReady()) { + utt = new Utt; + utt->utType = utScreenReader; + utt->sentence = m_speechData->getScreenReaderOutput(); + } else { + if (m_speechData->warningInQueue()) { + utt = new Utt; + utt->utType = utWarning; + utt->sentence = m_speechData->dequeueWarning(); + } else { + if (m_speechData->messageInQueue()) { + utt = new Utt; + utt->utType = utMessage; + utt->sentence = m_speechData->dequeueMessage(); + } else { + uint jobNum = m_lastJobNum; + uint seq = m_lastSeq; + mlText* sentence = m_speechData->getNextSentenceText(jobNum, seq); + // Skip over blank lines. + while (sentence && sentence->text.isEmpty()) + { + jobNum = sentence->jobNum; + seq = sentence->seq; + sentence = m_speechData->getNextSentenceText(jobNum, seq); + } + // If this utterance is already in the queue, it means we have run out of + // stuff to say and are trying to requeue already queued (and waiting stuff). + if (sentence && !isInUtteranceQueue(sentence->jobNum, sentence->seq)) + { + utt = new Utt; + utt->utType = utText; + utt->sentence = sentence; + } + } + } + } + if (utt) + { + gotOne = true; + utt->isSSML = isSsml(utt->sentence->text); + utt->state = usNone; + utt->audioPlayer = 0; + utt->audioStretcher = 0; + utt->audioUrl = QString::null; + utt->plugin = m_talkerMgr->talkerToPlugin(utt->sentence->talker); + // Save some time by setting initial state now. + setInitialUtteranceState(*utt); + // Screen Reader Outputs need to be processed ASAP. + if (utt->utType == utScreenReader) + { + m_uttQueue.insert(m_uttQueue.begin(), *utt); + // Delete any other Screen Reader Outputs in the queue. + // Only one Screen Reader Output at a time. + uttIterator it = m_uttQueue.begin(); + ++it; + while (it != m_uttQueue.end()) + { + if (it->utType == utScreenReader) + it = deleteUtterance(it); + else + ++it; + } + } + // If the new utterance is a Warning or Message... + if ((utt->utType == utWarning) || (utt->utType == utMessage)) + { + uttIterator itEnd = m_uttQueue.end(); + uttIterator it = m_uttQueue.begin(); + bool interrupting = false; + if (it != itEnd) + { + // New Warnings go after Screen Reader Output, other Warnings, + // Interruptions, and in-process text, + // but before Resumes, waiting text or signals. + if (utt->utType == utWarning) + while ( it != itEnd && + ((it->utType == utScreenReader) || + (it->utType == utWarning) || + (it->utType == utInterruptMsg) || + (it->utType == utInterruptSnd))) ++it; + // New Messages go after Screen Reader Output, Warnings, other Messages, + // Interruptions, and in-process text, + // but before Resumes, waiting text or signals. + if (utt->utType == utMessage) + while ( it != itEnd && + ((it->utType == utScreenReader) || + (it->utType == utWarning) || + (it->utType == utMessage) || + (it->utType == utInterruptMsg) || + (it->utType == utInterruptSnd))) ++it; + if (it != itEnd) + if (it->utType == utText && + ((it->state == usPlaying) || + (it->state == usSaying))) ++it; + // If now pointing at a text message, we are interrupting. + // Insert optional Interruption message and sound. + if (it != itEnd) interrupting = (it->utType == utText && it->state != usPaused); + if (interrupting) + { + if (m_speechData->textPreSndEnabled) + { + Utt intrUtt; + intrUtt.sentence = new mlText; + intrUtt.sentence->text = QString::null; + intrUtt.sentence->talker = utt->sentence->talker; + intrUtt.sentence->appId = utt->sentence->appId; + intrUtt.sentence->jobNum = utt->sentence->jobNum; + intrUtt.sentence->seq = 0; + intrUtt.audioUrl = m_speechData->textPreSnd; + intrUtt.audioPlayer = 0; + intrUtt.utType = utInterruptSnd; + intrUtt.isSSML = false; + intrUtt.state = usStretched; + intrUtt.plugin = 0; + it = m_uttQueue.insert(it, intrUtt); + ++it; + } + if (m_speechData->textPreMsgEnabled) + { + Utt intrUtt; + intrUtt.sentence = new mlText; + intrUtt.sentence->text = m_speechData->textPreMsg; + // Interruptions are spoken using default Talker. + intrUtt.sentence->talker = QString::null; + intrUtt.sentence->appId = utt->sentence->appId; + intrUtt.sentence->jobNum = utt->sentence->jobNum; + intrUtt.sentence->seq = 0; + intrUtt.audioUrl = QString::null; + intrUtt.audioPlayer = 0; + intrUtt.utType = utInterruptMsg; + intrUtt.isSSML = isSsml(intrUtt.sentence->text); + intrUtt.plugin = m_talkerMgr->talkerToPlugin(intrUtt.sentence->talker); + intrUtt.state = usNone; + setInitialUtteranceState(intrUtt); + it = m_uttQueue.insert(it, intrUtt); + ++it; + } + } + } + // Insert the new message or warning. + it = m_uttQueue.insert(it, *utt); + ++it; + // Resumption message and sound. + if (interrupting) + { + if (m_speechData->textPostSndEnabled) + { + Utt resUtt; + resUtt.sentence = new mlText; + resUtt.sentence->text = QString::null; + resUtt.sentence->talker = utt->sentence->talker; + resUtt.sentence->appId = utt->sentence->appId; + resUtt.sentence->jobNum = utt->sentence->jobNum; + resUtt.sentence->seq = 0; + resUtt.audioUrl = m_speechData->textPostSnd; + resUtt.audioPlayer = 0; + resUtt.utType = utResumeSnd; + resUtt.isSSML = false; + resUtt.state = usStretched; + resUtt.plugin = 0; + it = m_uttQueue.insert(it, resUtt); + ++it; + } + if (m_speechData->textPostMsgEnabled) + { + Utt resUtt; + resUtt.sentence = new mlText; + resUtt.sentence->text = m_speechData->textPostMsg; + resUtt.sentence->talker = QString::null; + resUtt.sentence->appId = utt->sentence->appId; + resUtt.sentence->jobNum = utt->sentence->jobNum; + resUtt.sentence->seq = 0; + resUtt.audioUrl = QString::null; + resUtt.audioPlayer = 0; + resUtt.utType = utResumeMsg; + resUtt.isSSML = isSsml(resUtt.sentence->text); + resUtt.plugin = m_talkerMgr->talkerToPlugin(resUtt.sentence->talker); + resUtt.state = usNone; + setInitialUtteranceState(resUtt); + it = m_uttQueue.insert(it, resUtt); + } + } + } + // If a text message... + if (utt->utType == utText) + { + // If job number has changed... + if (utt->sentence->jobNum != m_lastJobNum) + { + // If we finished the last job, append End-of-job to the queue, + // which will become a textFinished signal when it is processed. + if (m_lastJobNum) + { + if (m_lastSeq == static_cast<uint>(m_speechData->getTextCount(m_lastJobNum))) + { + Utt jobUtt; + jobUtt.sentence = new mlText; + jobUtt.sentence->text = QString::null; + jobUtt.sentence->talker = QString::null; + jobUtt.sentence->appId = m_lastAppId; + jobUtt.sentence->jobNum = m_lastJobNum; + jobUtt.sentence->seq = 0; + jobUtt.audioUrl = QString::null; + jobUtt.utType = utEndOfJob; + jobUtt.isSSML = false; + jobUtt.plugin = 0; + jobUtt.state = usWaitingSignal; + m_uttQueue.append(jobUtt); + } + } + m_lastJobNum = utt->sentence->jobNum; + m_lastAppId = utt->sentence->appId; + // If we are at beginning of new job, append Start-of-job to queue, + // which will become a textStarted signal when it is processed. + if (utt->sentence->seq == 1) + { + Utt jobUtt; + jobUtt.sentence = new mlText; + jobUtt.sentence->text = QString::null; + jobUtt.sentence->talker = QString::null; + jobUtt.sentence->appId = m_lastAppId; + jobUtt.sentence->jobNum = m_lastJobNum; + jobUtt.sentence->seq = utt->sentence->seq; + jobUtt.audioUrl = QString::null; + jobUtt.utType = utStartOfJob; + jobUtt.isSSML = false; + jobUtt.plugin = 0; + jobUtt.state = usWaitingSignal; + m_uttQueue.append(jobUtt); + } + } + m_lastSeq = utt->sentence->seq; + // Add the new utterance to the queue. + m_uttQueue.append(*utt); + } + delete utt; + } else { + // If no more text to speak, and we finished the last job, issue textFinished signal. + if (m_lastJobNum) + { + if (m_lastSeq == static_cast<uint>(m_speechData->getTextCount(m_lastJobNum))) + { + Utt jobUtt; + jobUtt.sentence = new mlText; + jobUtt.sentence->text = QString::null; + jobUtt.sentence->talker = QString::null; + jobUtt.sentence->appId = m_lastAppId; + jobUtt.sentence->jobNum = m_lastJobNum; + jobUtt.sentence->seq = 0; + jobUtt.audioUrl = QString::null; + jobUtt.utType = utEndOfJob; + jobUtt.isSSML = false; + jobUtt.plugin = 0; + jobUtt.state = usWaitingSignal; + m_uttQueue.append(jobUtt); + gotOne = true; + ++m_lastSeq; // Don't append another End-of-job + } + } + } + + return gotOne; +} + +/** + * Given an iterator pointing to the m_uttQueue, deletes the utterance + * from the queue. If the utterance is currently being processed by a + * plugin or the Audio Player, halts that operation and deletes Audio Player. + * Also takes care of deleting temporary audio file. + * @param it Iterator pointer to m_uttQueue. + * @return Iterator pointing to the next utterance in the + * queue, or m_uttQueue.end(). + */ +uttIterator Speaker::deleteUtterance(uttIterator it) +{ + switch (it->state) + { + case usNone: + case usWaitingTransform: + case usWaitingSay: + case usWaitingSynth: + case usWaitingSignal: + case usSynthed: + case usFinished: + case usStretched: + break; + + case usTransforming: + { + delete it->transformer; + it->transformer = 0; + break; + } + case usSaying: + case usSynthing: + { + // If plugin supports asynchronous mode, and it is busy, halt it. + PlugInProc* plugin = it->plugin; + if (it->plugin->supportsAsync()) + if ((plugin->getState() == psSaying) || (plugin->getState() == psSynthing)) + { + kdDebug() << "Speaker::deleteUtterance calling stopText" << endl; + plugin->stopText(); + } + break; + } + case usStretching: + { + delete it->audioStretcher; + it->audioStretcher = 0; + break; + } + case usPlaying: + { + m_timer->stop(); + it->audioPlayer->stop(); + delete it->audioPlayer; + break; + } + case usPaused: + case usPreempted: + { + // Note: Must call stop(), even if player not currently playing. Why? + it->audioPlayer->stop(); + delete it->audioPlayer; + break; + } + } + if (!it->audioUrl.isNull()) + { + // If the audio file was generated by a plugin, delete it. + if (it->plugin) + { + if (m_speechData->keepAudio) + { + QCString seqStr; + seqStr.sprintf("%08i", it->sentence->seq); // Zero-fill to 8 chars. + QCString jobStr; + jobStr.sprintf("%08i", it->sentence->jobNum); + QString dest = m_speechData->keepAudioPath + "/kttsd-" + + QString("%1-%2").arg(jobStr).arg(seqStr) + ".wav"; + QFile::remove(dest); + QDir d; + d.rename(it->audioUrl, dest); + // TODO: This is always producing the following. Why and how to fix? + // It moves the files just fine. + // kio (KIOJob): stat file:///home/kde-devel/.kde/share/apps/kttsd/audio/kttsd-5-1.wav + // kio (KIOJob): error 11 /home/kde-devel/.kde/share/apps/kttsd/audio/kttsd-5-1.wav + // kio (KIOJob): This seems to be a suitable case for trying to rename before stat+[list+]copy+del + // KIO::move(it->audioUrl, dest, false); + } + else + QFile::remove(it->audioUrl); + } + } + // Delete the utterance from queue. + delete it->sentence; + return m_uttQueue.erase(it); +} + +/** + * Given an iterator pointing to the m_uttQueue, starts playing audio if + * 1) An audio file is ready to be played, and + * 2) It is not already playing. + * If another audio player is already playing, pauses it before starting + * the new audio player. + * @param it Iterator pointer to m_uttQueue. + * @return True if an utterance began playing or resumed. + */ +bool Speaker::startPlayingUtterance(uttIterator it) +{ + // kdDebug() << "Speaker::startPlayingUtterance running" << endl; + if (it->state == usPlaying) return false; + if (it->audioUrl.isNull()) return false; + bool started = false; + // Pause (preempt) any other utterance currently being spoken. + // If any plugins are audibilizing, must wait for them to finish. + uttIterator itEnd = m_uttQueue.end(); + for (uttIterator it2 = m_uttQueue.begin(); it2 != itEnd; ++it2) + if (it2 != it) + { + if (it2->state == usPlaying) + { + m_timer->stop(); + it2->audioPlayer->pause(); + it2->state = usPreempted; + } + if (it2->state == usSaying) return false; + } + uttState utState = it->state; + switch (utState) + { + case usNone: + case usWaitingTransform: + case usTransforming: + case usWaitingSay: + case usWaitingSynth: + case usWaitingSignal: + case usSaying: + case usSynthing: + case usSynthed: + case usStretching: + case usPlaying: + case usFinished: + break; + + case usStretched: + { + // Don't start playback yet if text job is paused. + if ((it->utType != utText) || + (m_speechData->getTextJobState(it->sentence->jobNum) != KSpeech::jsPaused)) + { + + it->audioPlayer = createPlayerObject(); + if (it->audioPlayer) + { + it->audioPlayer->startPlay(it->audioUrl); + // Set job to speaking state and set sequence number. + mlText* sentence = it->sentence; + m_currentJobNum = sentence->jobNum; + m_speechData->setTextJobState(m_currentJobNum, KSpeech::jsSpeaking); + m_speechData->setJobSequenceNum(m_currentJobNum, sentence->seq); + prePlaySignals(it); + it->state = usPlaying; + if (!m_timer->start(timerInterval, FALSE)) + kdDebug() << "Speaker::startPlayingUtterance: timer.start failed" << endl; + started = true; + } else { + // If could not create audio player object, best we can do is silence. + it->state = usFinished; + } + } + break; + } + case usPaused: + { + // Unpause playback only if user has resumed. + // kdDebug() << "Speaker::startPlayingUtterance: checking whether to resume play" << endl; + if ((it->utType != utText) || + (m_speechData->getTextJobState(it->sentence->jobNum) != KSpeech::jsPaused)) + { + // kdDebug() << "Speaker::startPlayingUtterance: resuming play" << endl; + it->audioPlayer->startPlay(QString::null); // resume + it->state = usPlaying; + if (!m_timer->start(timerInterval, FALSE)) + kdDebug() << "Speaker::startPlayingUtterance: timer.start failed" << endl; + started = true; + } + break; + } + + case usPreempted: + { + // Preempted playback automatically resumes. + // Note: Must call stop(), even if player not currently playing. Why? + it->audioPlayer->startPlay(QString::null); // resume + it->state = usPlaying; + if (!m_timer->start(timerInterval, FALSE)) + kdDebug() << "Speaker::startPlayingUtterance: timer.start failed" << endl; + started = true; + break; + } + } + return started; +} + +/** + * Takes care of emitting reading interrupted/resumed and sentence started signals. + * Should be called just before audibilizing an utterance. + * @param it Iterator pointer to m_uttQueue. + * It also makes sure the job state is set to jsSpeaking. + */ +void Speaker::prePlaySignals(uttIterator it) +{ + uttType utType = it->utType; + if (utType == utText) + { + // If this utterance is for a regular text message, + // and it was interrupted, emit reading resumed signal. + if (m_textInterrupted) + { + m_textInterrupted = false; + emit readingResumed(); + } + // Set job to speaking state and set sequence number. + mlText* sentence = it->sentence; + // Emit sentence started signal. + emit sentenceStarted(sentence->text, + sentence->talker, sentence->appId, + m_currentJobNum, sentence->seq); + } else { + // If this utterance is not a regular text message, + // and we are doing a text job, emit reading interrupted signal. + if (isSpeakingText()) + { + m_textInterrupted = true; + emit readingInterrupted(); + } + } +} + +/** + * Takes care of emitting sentenceFinished signal. + * Should be called immediately after an utterance has completed playback. + * @param it Iterator pointer to m_uttQueue. + */ +void Speaker::postPlaySignals(uttIterator it) +{ + uttType utType = it->utType; + if (utType == utText) + { + // If this utterance is for a regular text message, + // emit sentence finished signal. + mlText* sentence = it->sentence; + emit sentenceFinished(sentence->appId, + sentence->jobNum, sentence->seq); + } +} + +/** + * Constructs a temporary filename for plugins to use as a suggested filename + * for synthesis to write to. + * @return Full pathname of suggested file. + */ +QString Speaker::makeSuggestedFilename() +{ + KTempFile tempFile (locateLocal("tmp", "kttsd-"), ".wav"); + QString waveFile = tempFile.file()->name(); + tempFile.close(); + QFile::remove(waveFile); + // kdDebug() << "Speaker::makeSuggestedFilename: Suggesting filename: " << waveFile << endl; + return KStandardDirs::realFilePath(waveFile); +} + +/** + * Creates and returns a player object based on user option. + */ +Player* Speaker::createPlayerObject() +{ + Player* player = 0; + QString plugInName; + switch(m_playerOption) + { + case 1 : + { + plugInName = "kttsd_gstplugin"; + break; + } + case 2 : + { + plugInName = "kttsd_alsaplugin"; + break; + } + case 3 : + { + plugInName = "kttsd_akodeplugin"; + break; + } + default: + { + plugInName = "kttsd_artsplugin"; + break; + } + } + KTrader::OfferList offers = KTrader::self()->query( + "KTTSD/AudioPlugin", QString("DesktopEntryName == '%1'").arg(plugInName)); + + if(offers.count() == 1) + { + kdDebug() << "Speaker::createPlayerObject: Loading " << offers[0]->library() << endl; + KLibFactory *factory = KLibLoader::self()->factory(offers[0]->library().latin1()); + if (factory) + player = + KParts::ComponentFactory::createInstanceFromLibrary<Player>( + offers[0]->library().latin1(), this, offers[0]->library().latin1()); + } + if (player == 0) + { + // If we tried for GStreamer or ALSA plugin and failed, fall back to aRts plugin. + if (m_playerOption != 0) + { + kdDebug() << "Speaker::createPlayerObject: Could not load " + plugInName + + " plugin. Falling back to aRts." << endl; + m_playerOption = 0; + return createPlayerObject(); + } + else + kdDebug() << "Speaker::createPlayerObject: Could not load aRts plugin. Is KDEDIRS set correctly?" << endl; + } else + // Must have GStreamer >= 0.8.7. If not, use aRts. + if (m_playerOption == 1) + { + if (!player->requireVersion(0, 8, 7)) + { + delete player; + m_playerOption = 0; + return createPlayerObject(); + } + } + if (player) { + player->setSinkName(m_sinkName); + player->setPeriodSize(m_periodSize); + player->setPeriods(m_periodSize); + player->setDebugLevel(m_playerDebugLevel); + } + return player; +} + + +/* Slots ==========================================================*/ + +/** +* Received from PlugIn objects when they finish asynchronous synthesis +* and audibilizing. +*/ +void Speaker::slotSayFinished() +{ + // Since this signal handler may be running from a plugin's thread, + // convert to postEvent and return immediately. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 101); + QApplication::postEvent(this, ev); +} + +/** +* Received from PlugIn objects when they finish asynchronous synthesis. +*/ +void Speaker::slotSynthFinished() +{ + // Since this signal handler may be running from a plugin's thread, + // convert to postEvent and return immediately. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 102); + QApplication::postEvent(this, ev); +} + +/** +* Received from PlugIn objects when they asynchronously stopText. +*/ +void Speaker::slotStopped() +{ + // Since this signal handler may be running from a plugin's thread, + // convert to postEvent and return immediately. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 103); + QApplication::postEvent(this, ev); +} + +/** +* Received from audio stretcher when stretching (speed adjustment) is finished. +*/ +void Speaker::slotStretchFinished() +{ + // Convert to postEvent and return immediately. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 104); + QApplication::postEvent(this, ev); +} + +/** +* Received from transformer (SSMLConvert) when transforming is finished. +*/ +void Speaker::slotTransformFinished() +{ + // Convert to postEvent and return immediately. + QCustomEvent* ev = new QCustomEvent(QEvent::User + 105); + QApplication::postEvent(this, ev); +} + +/** Received from PlugIn object when they encounter an error. +* @param keepGoing True if the plugin can continue processing. +* False if the plugin cannot continue, for example, +* the speech engine could not be started. +* @param msg Error message. +*/ +void Speaker::slotError(bool /*keepGoing*/, const QString& /*msg*/) +{ + // Since this signal handler may be running from a plugin's thread, + // convert to postEvent and return immediately. + // TODO: Do something with error messages. + /* + if (keepGoing) + QCustomEvent* ev = new QCustomEvent(QEvent::User + 106); + else + QCustomEvent* ev = new QCustomEvent(QEvent::User + 107); + QApplication::postEvent(this, ev); + */ +} + +/** +* Received from Timer when it fires. +* Check audio player to see if it is finished. +*/ +void Speaker::slotTimeout() +{ + uttIterator itEnd = m_uttQueue.end(); + for (uttIterator it = m_uttQueue.begin(); it != itEnd; ++it) + { + if (it->state == usPlaying) + { + if (it->audioPlayer->playing()) return; // Still playing. + m_timer->stop(); + postPlaySignals(it); + deleteUtterance(it); + doUtterances(); + return; + } + } +} + +/** +* Processes events posted by plugins. When asynchronous plugins emit signals +* they are converted into these events. +*/ +bool Speaker::event ( QEvent * e ) +{ + // TODO: Do something with event numbers 106 (error; keepGoing=True) + // and 107 (error; keepGoing=False). + if ((e->type() >= (QEvent::User + 101)) && (e->type() <= (QEvent::User + 105))) + { + // kdDebug() << "Speaker::event: received event." << endl; + doUtterances(); + return TRUE; + } + else return FALSE; +} + diff --git a/kttsd/kttsd/speaker.h b/kttsd/kttsd/speaker.h new file mode 100644 index 0000000..b72e832 --- /dev/null +++ b/kttsd/kttsd/speaker.h @@ -0,0 +1,599 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Speaker class. + + This class is in charge of getting the messages, warnings and text from + the queue and call the plug ins function to actually speak the texts. + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + ******************************************************************************/ + +/****************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License. * + * * + ******************************************************************************/ + +#ifndef _SPEAKER_H_ +#define _SPEAKER_H_ + +// Qt includes. +#include <qobject.h> +#include <qvaluevector.h> +#include <qevent.h> + +// KTTSD includes. +#include <speechdata.h> +#include <pluginproc.h> +#include <stretcher.h> +#include <talkercode.h> +#include <ssmlconvert.h> + +class Player; +class QTimer; +class TalkerMgr; + +/** +* Type of utterance. +*/ +enum uttType +{ + utText, /**< Text */ + utInterruptMsg, /**< Interruption text message */ + utInterruptSnd, /**< Interruption sound file */ + utResumeMsg, /**< Resume text message */ + utResumeSnd, /**< Resume sound file */ + utMessage, /**< Message */ + utWarning, /**< Warning */ + utScreenReader, /**< Screen Reader Output */ + utStartOfJob, /**< Start-of-job */ + utEndOfJob /**< End-of-job */ +}; + +/** +* Processing state of an utterance. +*/ +enum uttState +{ + usNone, /**< Null state. Brand new utterance. */ + usWaitingTransform, /**< Waiting to be transformed (XSLT) */ + usTransforming, /**< Transforming the utterance (XSLT). */ + usWaitingSay, /**< Waiting to start synthesis. */ + usWaitingSynth, /**< Waiting to be synthesized and audibilized. */ + usWaitingSignal, /**< Waiting to emit a textStarted or textFinished signal. */ + usSaying, /**< Plugin is synthesizing and audibilizing. */ + usSynthing, /**< Plugin is synthesizing only. */ + usSynthed, /**< Plugin has finished synthesizing. Ready for stretch. */ + usStretching, /**< Adjusting speed. */ + usStretched, /**< Speed adjustment finished. Ready for playback. */ + usPlaying, /**< Playing on Audio Player. */ + usPaused, /**< Paused on Audio Player due to user action. */ + usPreempted, /**< Paused on Audio Player due to Screen Reader Output. */ + usFinished /**< Ready for deletion. */ +}; + +/** +* Structure containing an utterance being synthesized or audibilized. +*/ +struct Utt{ + mlText* sentence; /* The text, talker, appId, and sequence num. */ + uttType utType; /* The type of utterance (text, msg, screen reader) */ + bool isSSML; /* True if the utterance contains SSML markup. */ + uttState state; /* Processing state of the utterance. */ + SSMLConvert* transformer; /* XSLT transformer. */ + PlugInProc* plugin; /* The plugin that synthesizes the utterance. */ + Stretcher* audioStretcher; /* Audio stretcher object. Adjusts speed. */ + QString audioUrl; /* Filename containing synthesized audio. Null if + plugin has not yet synthesized the utterance, or if + plugin does not support synthesis. */ + Player* audioPlayer; /* The audio player audibilizing the utterance. Null + if not currently audibilizing or if plugin doesn't + support synthesis. */ +}; + +/** +* Iterator for queue of utterances. +*/ +typedef QValueVector<Utt>::iterator uttIterator; + +// Timer interval for checking whether audio playback is finished. +const int timerInterval = 500; + +/** + * This class is in charge of getting the messages, warnings and text from + * the queue and call the plug ins function to actually speak the texts. + */ +class Speaker : public QObject{ + Q_OBJECT + + public: + /** + * Constructor + * Calls load plug ins + */ + Speaker(SpeechData* speechData, TalkerMgr* talkerMgr, + QObject *parent = 0, const char *name = 0); + + /** + * Destructor + */ + ~Speaker(); + + /** + * Tells the thread to exit + */ + void requestExit(); + + /** + * Main processing loop. Dequeues utterances and sends them to the + * plugins and/or Audio Player. + */ + void doUtterances(); + + /** + * Determine if kttsd is currently speaking any text jobs. + * @return True if currently speaking any text jobs. + */ + bool isSpeakingText(); + + /** + * Get the job number of the current text job. + * @return Job number of the current text job. 0 if no jobs. + * + * Note that the current job may not be speaking. See @ref isSpeakingText. + * @see getTextJobState. + * @see isSpeakingText + */ + uint getCurrentTextJob(); + + /** + * Remove a text job from the queue. + * @param jobNum Job number of the text job. + * + * The job is deleted from the queue and the @ref textRemoved signal is emitted. + * + * If there is another job in the text queue, and it is marked speakable, + * that job begins speaking. + */ + void removeText(const uint jobNum); + + /** + * Start a text job at the beginning. + * @param jobNum Job number of the text job. + * + * Rewinds the job to the beginning. + * + * The job is marked speakable. + * If there are other speakable jobs preceeding this one in the queue, + * those jobs continue speaking and when finished, this job will begin speaking. + * If there are no other speakable jobs preceeding this one, it begins speaking. + * + * The @ref textStarted signal is emitted when the text job begins speaking. + * When all the sentences of the job have been spoken, the job is marked for deletion from + * the text queue and the @ref textFinished signal is emitted. + */ + void startText(const uint jobNum); + + /** + * Stop a text job and rewind to the beginning. + * @param jobNum Job number of the text job. + * + * The job is marked not speakable and will not be speakable until @ref startText or @ref resumeText + * is called. + * + * If there are speaking jobs preceeding this one in the queue, they continue speaking. + * If the job is currently speaking, the @ref textStopped signal is emitted and the job stops speaking. + * Depending upon the speech engine and plugin used, speeking may not stop immediately + * (it might finish the current sentence). + */ + void stopText(const uint jobNum); + + /** + * Pause a text job. + * @param jobNum Job number of the text job. + * + * The job is marked as paused and will not be speakable until @ref resumeText or + * @ref startText is called. + * + * If there are speaking jobs preceeding this one in the queue, they continue speaking. + * If the job is currently speaking, the @ref textPaused signal is emitted and the job stops speaking. + * Depending upon the speech engine and plugin used, speeking may not stop immediately + * (it might finish the current sentence). + * @see resumeText + */ + void pauseText(const uint jobNum); + + /** + * Start or resume a text job where it was paused. + * @param jobNum Job number of the text job. + * + * The job is marked speakable. + * + * If the job is currently speaking, or is waiting to be spoken (speakable + * state), the resumeText() call is ignored. + * + * If the job is currently queued, or is finished, it is the same as calling + * @ref startText . + * + * If there are speaking jobs preceeding this one in the queue, those jobs continue speaking and, + * when finished this job will begin speaking where it left off. + * + * The @ref textResumed signal is emitted when the job resumes. + * @see pauseText + */ + void resumeText(const uint jobNum); + + /** + * Move a text job down in the queue so that it is spoken later. + * @param jobNum Job number of the text job. + * + * If the job is currently speaking, it is paused. + * If the next job in the queue is speakable, it begins speaking. + */ + void moveTextLater(const uint jobNum); + + /** + * Jump to the first sentence of a specified part of a text job. + * @param partNum Part number of the part to jump to. Parts are numbered starting at 1. + * @param jobNum Job number of the text job. + * @return Part number of the part actually jumped to. + * + * If partNum is greater than the number of parts in the job, jumps to last part. + * If partNum is 0, does nothing and returns the current part number. + * If no such job, does nothing and returns 0. + * Does not affect the current speaking/not-speaking state of the job. + */ + int jumpToTextPart(const int partNum, const uint jobNum); + + /** + * Advance or rewind N sentences in a text job. + * @param n Number of sentences to advance (positive) or rewind (negative) + * in the job. + * @param jobNum Job number of the text job. + * @return Sequence number of the sentence actually moved to. + * Sequence numbers are numbered starting at 1. + * + * If no such job, does nothing and returns 0. + * If n is zero, returns the current sequence number of the job. + * Does not affect the current speaking/not-speaking state of the job. + */ + uint moveRelTextSentence(const int n, const uint jobNum); + + signals: + /** + * Emitted whenever reading a text was started or resumed + */ + void readingStarted(); + + /** + * Emitted whenever reading a text was finished, + * or paused, or stopped before it was finished + */ + void readingStopped(); + + /** + * Emitted whenever a message or warning interrupts reading a text + */ + void readingInterrupted(); + + /** + * Emitted whenever reading a text is resumed after it was interrupted + * Note: In function resumeText, readingStarted is called instead + */ + void readingResumed(); + + /* The following signals correspond to the signals in the KSpeech interface. */ + + /** + * This signal is emitted when the speech engine/plugin encounters a marker in the text. + * @param appId DCOP application ID of the application that queued the text. + * @param markerName The name of the marker seen. + * @see markers + */ + void markerSeen(const QCString& appId, const QString& markerName); + + /** + * This signal is emitted whenever a sentence begins speaking. + * @param appId DCOP application ID of the application that queued the text. + * @param jobNum Job number of the text job. + * @param seq Sequence number of the text. + */ + void sentenceStarted(QString text, QString language, const QCString& appId, + const uint jobNum, const uint seq); + + /** + * This signal is emitted when a sentence has finished speaking. + * @param appId DCOP application ID of the application that queued the text. + * @param jobNum Job number of the text job. + * @param seq Sequence number of the text. + */ + void sentenceFinished(const QCString& appId, const uint jobNum, const uint seq); + + /** + * This signal is emitted whenever speaking of a text job begins. + * @param appId The DCOP senderId of the application that created the job. NULL if kttsd. + * @param jobNum Job number of the text job. + */ + void textStarted(const QCString& appId, const uint jobNum); + + /** + * This signal is emitted whenever a text job is finished. The job has + * been marked for deletion from the queue and will be deleted when another + * job reaches the Finished state. (Only one job in the text queue may be + * in state Finished at one time.) If @ref startText or @ref resumeText is + * called before the job is deleted, it will remain in the queue for speaking. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + */ + void textFinished(const QCString& appId, const uint jobNum); + + /** + * This signal is emitted whenever a speaking text job stops speaking. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + */ + void textStopped(const QCString& appId, const uint jobNum); + /** + * This signal is emitted whenever a speaking text job is paused. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + */ + void textPaused(const QCString& appId, const uint jobNum); + /** + * This signal is emitted when a text job, that was previously paused, resumes speaking. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + */ + void textResumed(const QCString& appId, const uint jobNum); + + protected: + /** + * Processes events posted by ThreadedPlugIns. + */ + virtual bool event ( QEvent * e ); + + private slots: + /** + * Received from PlugIn objects when they finish asynchronous synthesis. + */ + void slotSynthFinished(); + /** + * Received from PlugIn objects when they finish asynchronous synthesis + * and audibilizing. + */ + void slotSayFinished(); + /** + * Received from PlugIn objects when they asynchronously stopText. + */ + void slotStopped(); + /** + * Received from audio stretcher when stretching (speed adjustment) is finished. + */ + void slotStretchFinished(); + /** + * Received from transformer (SSMLConvert) when transforming is finished. + */ + void slotTransformFinished(); + /** Received from PlugIn object when they encounter an error. + * @param keepGoing True if the plugin can continue processing. + * False if the plugin cannot continue, for example, + * the speech engine could not be started. + * @param msg Error message. + */ + void slotError(bool keepGoing, const QString &msg); + /** + * Received from Timer when it fires. + * Check audio player to see if it is finished. + */ + void slotTimeout(); + + private: + + /** + * Converts an utterance state enumerator to a displayable string. + * @param state Utterance state. + * @return Displayable string for utterance state. + */ + QString uttStateToStr(uttState state); + + /** + * Converts an utterance type enumerator to a displayable string. + * @param utType Utterance type. + * @return Displayable string for utterance type. + */ + QString uttTypeToStr(uttType utType); + + /** + * Converts a plugin state enumerator to a displayable string. + * @param state Plugin state. + * @return Displayable string for plugin state. + */ + QString pluginStateToStr(pluginState state); + + /** + * Converts a job state enumerator to a displayable string. + * @param state Job state. + * @return Displayable string for job state. + */ + QString jobStateToStr(int state); + + /** + * Determines whether the given text is SSML markup. + */ + bool isSsml(const QString &text); + + /** + * Determines the initial state of an utterance. If the utterance contains + * SSML, the state is set to usWaitingTransform. Otherwise, if the plugin + * supports async synthesis, sets to usWaitingSynth, otherwise usWaitingSay. + * If an utterance has already been transformed, usWaitingTransform is + * skipped to either usWaitingSynth or usWaitingSay. + * @param utt The utterance. + */ + void setInitialUtteranceState(Utt &utt); + + /** + * Returns true if the given job and sequence number is already in the utterance queue. + */ + bool isInUtteranceQueue(uint jobNum, uint seqNum); + + /** + * Gets the next utterance to be spoken from speechdata and adds it to the queue. + * @return True if one or more utterances were added to the queue. + * + * Checks for waiting ScreenReaderOutput, Warnings, Messages, or Text, + * in that order. + * If Warning or Message and interruption messages have been configured, + * adds those to the queue as well. + * Determines which plugin should be used for the utterance. + */ + bool getNextUtterance(); + + /** + * Given an iterator pointing to the m_uttQueue, deletes the utterance + * from the queue. If the utterance is currently being processed by a + * plugin or the Audio Player, halts that operation and deletes Audio Player. + * Also takes care of deleting temporary audio file. + * @param it Iterator pointer to m_uttQueue. + * @return Iterator pointing to the next utterance in the + * queue, or m_uttQueue.end(). + */ + uttIterator deleteUtterance(uttIterator it); + + /** + * Given an iterator pointing to the m_uttQueue, starts playing audio if + * 1) An audio file is ready to be played, and + * 2) It is not already playing. + * If another audio player is already playing, pauses it before starting + * the new audio player. + * @param it Iterator pointer to m_uttQueue. + * @return True if an utterance began playing or resumed. + */ + bool startPlayingUtterance(uttIterator it); + + /** + * Delete any utterances in the queue with this jobNum. + * @param jobNum The Job Number of the utterance(s) to delete. + * If currently processing any deleted utterances, stop them. + */ + void deleteUtteranceByJobNum(const uint jobNum); + + /** + * Pause the utterance with this jobNum and if it is playing on the Audio Player, + * pause the Audio Player. + * @param jobNum The Job Number of the utterance to pause. + */ + void pauseUtteranceByJobNum(const uint jobNum); + + /** + * Takes care of emitting reading interrupted/resumed and sentence started signals. + * Should be called just before audibilizing an utterance. + * @param it Iterator pointer to m_uttQueue. + */ + void prePlaySignals(uttIterator it); + + /** + * Takes care of emitting sentenceFinished signal. + * Should be called immediately after an utterance has completed playback. + * @param it Iterator pointer to m_uttQueue. + */ + void postPlaySignals(uttIterator it); + + /** + * Constructs a temporary filename for plugins to use as a suggested filename + * for synthesis to write to. + * @return Full pathname of suggested file. + */ + QString makeSuggestedFilename(); + + /** + * Creates and returns a player object based on user option. + */ + Player* createPlayerObject(); + + /** + * SpeechData local pointer + */ + SpeechData* m_speechData; + + /** + * TalkerMgr local pointer. + */ + TalkerMgr* m_talkerMgr; + + /** + * True if the speaker was requested to exit. + */ + volatile bool m_exitRequested; + + /** + * Queue of utterances we are currently processing. + */ + QValueVector<Utt> m_uttQueue; + + /** + * True when text job reading has been interrupted. + */ + bool m_textInterrupted; + + /** + * Used to prevent doUtterances from prematurely exiting. + */ + bool m_again; + + /** + * Which audio player to use. + * 0 = aRts + * 1 = gstreamer + * 2 = ALSA + */ + int m_playerOption; + + /** + * Audio stretch factor (Speed). + */ + float m_audioStretchFactor; + + /** + * GStreamer sink name to use, or ALSA PCM device name. + */ + QString m_sinkName; + + /** + * Timer for monitoring audio player. + */ + QTimer* m_timer; + + /** + * Current Text job being processed. + */ + uint m_currentJobNum; + + /** + * Job Number, appId, and sequence number of the last text sentence queued. + */ + uint m_lastJobNum; + QCString m_lastAppId; + uint m_lastSeq; + + /** + * Some parameters used by ALSA plugin. + * Size of buffer interrupt period (in frames) + * Number of periods in buffer. + */ + uint m_periodSize; + uint m_periods; + + /** + * Debug level in players. + */ + uint m_playerDebugLevel; +}; + +#endif // _SPEAKER_H_ diff --git a/kttsd/kttsd/speechdata.cpp b/kttsd/kttsd/speechdata.cpp new file mode 100644 index 0000000..a1bf26e --- /dev/null +++ b/kttsd/kttsd/speechdata.cpp @@ -0,0 +1,1275 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + This contains the SpeechData class which is in charge of maintaining + all the data on the memory. + It maintains queues manages the text. + We could say that this is the common repository between the KTTSD class + (dcop service) and the Speaker class (speaker, loads plug ins, call plug in + functions) + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + (C) 2004-2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + ******************************************************************************/ + +/****************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License. * + * * + ******************************************************************************/ + +// C++ includes. +#include <stdlib.h> + +// Qt includes. +#include <qregexp.h> +#include <qpair.h> +#include <qvaluelist.h> +#include <qdom.h> +#include <qfile.h> + +// KDE includes. +#include <kdebug.h> +#include <kglobal.h> +#include <kstandarddirs.h> +#include <kapplication.h> + +// KTTS includes. +#include "talkermgr.h" +#include "notify.h" + +// SpeechData includes. +#include "speechdata.h" +#include "speechdata.moc" + +// Set this to 1 to turn off filter support, including SBD as a plugin. +#define NO_FILTERS 0 + +/** +* Constructor +* Sets text to be stopped and warnings and messages queues to be autodelete. +*/ +SpeechData::SpeechData(){ + // kdDebug() << "Running: SpeechData::SpeechData()" << endl; + // The text should be stoped at the beggining (thread safe) + jobCounter = 0; + config = 0; + textJobs.setAutoDelete(true); + supportsHTML = false; + + // Warnings queue to be autodelete (thread safe) + warnings.setAutoDelete(true); + + // Messages queue to be autodelete (thread safe) + messages.setAutoDelete(true); + + screenReaderOutput.jobNum = 0; + screenReaderOutput.text = ""; +} + +bool SpeechData::readConfig(){ + // Load configuration + delete config; + //config = KGlobal::config(); + config = new KConfig("kttsdrc"); + + // Set the group general for the configuration of KTTSD itself (no plug ins) + config->setGroup("General"); + + // Load the configuration of the text interruption messages and sound + textPreMsgEnabled = config->readBoolEntry("TextPreMsgEnabled", false); + textPreMsg = config->readEntry("TextPreMsg"); + + textPreSndEnabled = config->readBoolEntry("TextPreSndEnabled", false); + textPreSnd = config->readEntry("TextPreSnd"); + + textPostMsgEnabled = config->readBoolEntry("TextPostMsgEnabled", false); + textPostMsg = config->readEntry("TextPostMsg"); + + textPostSndEnabled = config->readBoolEntry("TextPostSndEnabled", false); + textPostSnd = config->readEntry("TextPostSnd"); + keepAudio = config->readBoolEntry("KeepAudio", false); + keepAudioPath = config->readEntry("KeepAudioPath", locateLocal("data", "kttsd/audio/")); + + // Notification (KNotify). + notify = config->readBoolEntry("Notify", false); + notifyExcludeEventsWithSound = config->readBoolEntry("ExcludeEventsWithSound", true); + loadNotifyEventsFromFile( locateLocal("config", "kttsd_notifyevents.xml"), true ); + + // KTTSMgr auto start and auto exit. + autoStartManager = config->readBoolEntry("AutoStartManager", false); + autoExitManager = config->readBoolEntry("AutoExitManager", false); + + // Clear the pool of filter managers so that filters re-init themselves. + QPtrListIterator<PooledFilterMgr> it( m_pooledFilterMgrs ); + for( ; it.current(); ++it ) + { + PooledFilterMgr* pooledFilterMgr = it.current(); + delete pooledFilterMgr->filterMgr; + delete pooledFilterMgr->talkerCode; + delete pooledFilterMgr; + } + m_pooledFilterMgrs.clear(); + + // Create an initial FilterMgr for the pool to save time later. + PooledFilterMgr* pooledFilterMgr = new PooledFilterMgr(); + FilterMgr* filterMgr = new FilterMgr(); + filterMgr->init(config, "General"); + supportsHTML = filterMgr->supportsHTML(); + pooledFilterMgr->filterMgr = filterMgr; + pooledFilterMgr->busy = false; + pooledFilterMgr->job = 0; + pooledFilterMgr->partNum = 0; + // Connect signals from FilterMgr. + connect (filterMgr, SIGNAL(filteringFinished()), this, SLOT(slotFilterMgrFinished())); + connect (filterMgr, SIGNAL(filteringStopped()), this, SLOT(slotFilterMgrStopped())); + m_pooledFilterMgrs.append(pooledFilterMgr); + + return true; +} + +/** + * Loads notify events from a file. Clearing data if clear is True. + */ +void SpeechData::loadNotifyEventsFromFile( const QString& filename, bool clear) +{ + // Open existing event list. + QFile file( filename ); + if ( !file.open( IO_ReadOnly ) ) + { + kdDebug() << "SpeechData::loadNotifyEventsFromFile: Unable to open file " << filename << endl; + } + // QDomDocument doc( "http://www.kde.org/share/apps/kttsd/stringreplacer/wordlist.dtd []" ); + QDomDocument doc( "" ); + if ( !doc.setContent( &file ) ) { + file.close(); + kdDebug() << "SpeechData::loadNotifyEventsFromFile: File not in proper XML format. " << filename << endl; + } + // kdDebug() << "StringReplacerConf::load: document successfully parsed." << endl; + file.close(); + + if ( clear ) + { + notifyDefaultPresent = NotifyPresent::Passive; + notifyDefaultOptions.action = NotifyAction::SpeakMsg; + notifyDefaultOptions.talker = QString::null; + notifyDefaultOptions.customMsg = QString::null; + notifyAppMap.clear(); + } + + // Event list. + QDomNodeList eventList = doc.elementsByTagName("notifyEvent"); + const int eventListCount = eventList.count(); + for (int eventIndex = 0; eventIndex < eventListCount; ++eventIndex) + { + QDomNode eventNode = eventList.item(eventIndex); + QDomNodeList propList = eventNode.childNodes(); + QString eventSrc; + QString event; + QString actionName; + QString message; + TalkerCode talkerCode; + const int propListCount = propList.count(); + for (int propIndex = 0; propIndex < propListCount; ++propIndex) + { + QDomNode propNode = propList.item(propIndex); + QDomElement prop = propNode.toElement(); + if (prop.tagName() == "eventSrc") eventSrc = prop.text(); + if (prop.tagName() == "event") event = prop.text(); + if (prop.tagName() == "action") actionName = prop.text(); + if (prop.tagName() == "message") message = prop.text(); + if (prop.tagName() == "talker") talkerCode = TalkerCode(prop.text(), false); + } + NotifyOptions notifyOptions; + notifyOptions.action = NotifyAction::action( actionName ); + notifyOptions.talker = talkerCode.getTalkerCode(); + notifyOptions.customMsg = message; + if ( eventSrc != "default" ) + { + notifyOptions.eventName = NotifyEvent::getEventName( eventSrc, event ); + NotifyEventMap notifyEventMap = notifyAppMap[ eventSrc ]; + notifyEventMap[ event ] = notifyOptions; + notifyAppMap[ eventSrc ] = notifyEventMap; + } else { + notifyOptions.eventName = QString::null; + notifyDefaultPresent = NotifyPresent::present( event ); + notifyDefaultOptions = notifyOptions; + } + } +} + +/** +* Destructor +*/ +SpeechData::~SpeechData(){ + // kdDebug() << "Running: SpeechData::~SpeechData()" << endl; + // Walk through jobs and emit a textRemoved signal for each job. + for (mlJob* job = textJobs.first(); (job); job = textJobs.next()) + { + emit textRemoved(job->appId, job->jobNum); + } + + QPtrListIterator<PooledFilterMgr> it( m_pooledFilterMgrs ); + for( ; it.current(); ++it ) + { + PooledFilterMgr* pooledFilterMgr = it.current(); + delete pooledFilterMgr->filterMgr; + delete pooledFilterMgr->talkerCode; + delete pooledFilterMgr; + } + + delete config; +} + +/** +* Say a message as soon as possible, interrupting any other speech in progress. +* IMPORTANT: This method is reserved for use by Screen Readers and should not be used +* by any other applications. +* @param msg The message to be spoken. +* @param talker Code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* +* If an existing Screen Reader output is in progress, it is stopped and discarded and +* replaced with this new message. +*/ +void SpeechData::setScreenReaderOutput(const QString &msg, const QString &talker, const QCString &appId) +{ + screenReaderOutput.text = msg; + screenReaderOutput.talker = talker; + screenReaderOutput.appId = appId; + screenReaderOutput.seq = 1; +} + +/** +* Retrieves the Screen Reader Output. +*/ +mlText* SpeechData::getScreenReaderOutput() +{ + mlText* temp = new mlText(); + temp->text = screenReaderOutput.text; + temp->talker = screenReaderOutput.talker; + temp->appId = screenReaderOutput.appId; + temp->seq = screenReaderOutput.seq; + // Blank the Screen Reader to text to "empty" it. + screenReaderOutput.text = ""; + return temp; +} + +/** +* Returns true if Screen Reader Output is ready to be spoken. +*/ +bool SpeechData::screenReaderOutputReady() +{ + return !screenReaderOutput.text.isEmpty(); +} + +/** +* Add a new warning to the queue. +*/ +void SpeechData::enqueueWarning( const QString &warning, const QString &talker, const QCString &appId){ + // kdDebug() << "Running: SpeechData::enqueueWarning( const QString &warning )" << endl; + mlJob* job = new mlJob(); + ++jobCounter; + if (jobCounter == 0) ++jobCounter; // Overflow is OK, but don't want any 0 jobNums. + uint jobNum = jobCounter; + job->jobNum = jobNum; + job->talker = talker; + job->appId = appId; + job->seq = 1; + job->partCount = 1; + warnings.enqueue( job ); + job->sentences = QStringList(); + // Do not apply Sentence Boundary Detection filters to warnings. + startJobFiltering( job, warning, true ); + // uint count = warnings.count(); + // kdDebug() << "Adding '" << temp->text << "' with talker '" << temp->talker << "' from application " << appId << " to the warnings queue leaving a total of " << count << " items." << endl; +} + +/** +* Pop (get and erase) a warning from the queue. +* @return Pointer to mlText structure containing the message. +* +* Caller is responsible for deleting the structure. +*/ +mlText* SpeechData::dequeueWarning(){ + // kdDebug() << "Running: SpeechData::dequeueWarning()" << endl; + mlJob* job = warnings.dequeue(); + waitJobFiltering(job); + mlText* temp = new mlText(); + temp->jobNum = job->jobNum; + temp->text = job->sentences.join(""); + temp->talker = job->talker; + temp->appId = job->appId; + temp->seq = 1; + delete job; + // uint count = warnings.count(); + // kdDebug() << "Removing '" << temp->text << "' with talker '" << temp->talker << "' from the warnings queue leaving a total of " << count << " items." << endl; + return temp; +} + +/** +* Are there any Warnings? +*/ +bool SpeechData::warningInQueue(){ + // kdDebug() << "Running: SpeechData::warningInQueue() const" << endl; + bool temp = !warnings.isEmpty(); + // if(temp){ + // kdDebug() << "The warnings queue is NOT empty" << endl; + // } else { + // kdDebug() << "The warnings queue is empty" << endl; + // } + return temp; +} + +/** +* Add a new message to the queue. +*/ +void SpeechData::enqueueMessage( const QString &message, const QString &talker, const QCString& appId){ + // kdDebug() << "Running: SpeechData::enqueueMessage" << endl; + mlJob* job = new mlJob(); + ++jobCounter; + if (jobCounter == 0) ++jobCounter; // Overflow is OK, but don't want any 0 jobNums. + uint jobNum = jobCounter; + job->jobNum = jobNum; + job->talker = talker; + job->appId = appId; + job->seq = 1; + job->partCount = 1; + messages.enqueue( job ); + job->sentences = QStringList(); + // Do not apply Sentence Boundary Detection filters to messages. + startJobFiltering( job, message, true ); + // uint count = messages.count(); + // kdDebug() << "Adding '" << temp->text << "' with talker '" << temp->talker << "' from application " << appId << " to the messages queue leaving a total of " << count << " items." << endl; +} + +/** +* Pop (get and erase) a message from the queue. +* @return Pointer to mlText structure containing the message. +* +* Caller is responsible for deleting the structure. +*/ +mlText* SpeechData::dequeueMessage(){ + // kdDebug() << "Running: SpeechData::dequeueMessage()" << endl; + mlJob* job = messages.dequeue(); + waitJobFiltering(job); + mlText* temp = new mlText(); + temp->jobNum = job->jobNum; + temp->text = job->sentences.join(""); + temp->talker = job->talker; + temp->appId = job->appId; + temp->seq = 1; + delete job; + /* mlText *temp = messages.dequeue(); */ + // uint count = messages.count(); + // kdDebug() << "Removing '" << temp->text << "' with talker '" << temp->talker << "' from the messages queue leaving a total of " << count << " items." << endl; + return temp; +} + +/** +* Are there any Messages? +*/ +bool SpeechData::messageInQueue(){ + // kdDebug() << "Running: SpeechData::messageInQueue() const" << endl; + bool temp = !messages.isEmpty(); + // if(temp){ + // kdDebug() << "The messages queue is NOT empty" << endl; + // } else { + // kdDebug() << "The messages queue is empty" << endl; + // } + return temp; +} + +/** +* Determines whether the given text is SSML markup. +*/ +bool SpeechData::isSsml(const QString &text) +{ + /// This checks to see if the root tag of the text is a <speak> tag. + QDomDocument ssml; + ssml.setContent(text, false); // No namespace processing. + /// Check to see if this is SSML + QDomElement root = ssml.documentElement(); + return (root.tagName() == "speak"); +} + +/** +* Parses a block of text into sentences using the application-specified regular expression +* or (if not specified), the default regular expression. +* @param text The message to be spoken. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* @return List of parsed sentences. +* +* If the text contains SSML, it is not parsed into sentences at all. +* TODO: Need a way to preserve SSML but still parse into sentences. +* We will walk before we run for now and not sentence parse. +*/ + +QStringList SpeechData::parseText(const QString &text, const QCString &appId /*=NULL*/) +{ + // There has to be a better way + // kdDebug() << "I'm getting: " << endl << text << " from application " << appId << endl; + if (isSsml(text)) + { + QString tempList(text); + return tempList; + } + // See if app has specified a custom sentence delimiter and use it, otherwise use default. + QRegExp sentenceDelimiter; + if (sentenceDelimiters.find(appId) != sentenceDelimiters.end()) + sentenceDelimiter = QRegExp(sentenceDelimiters[appId]); + else + sentenceDelimiter = QRegExp("([\\.\\?\\!\\:\\;]\\s)|(\\n *\\n)"); + QString temp = text; + // Replace spaces, tabs, and formfeeds with a single space. + temp.replace(QRegExp("[ \\t\\f]+"), " "); + // Replace sentence delimiters with tab. + temp.replace(sentenceDelimiter, "\\1\t"); + // Replace remaining newlines with spaces. + temp.replace("\n"," "); + temp.replace("\r"," "); + // Remove leading spaces. + temp.replace(QRegExp("\\t +"), "\t"); + // Remove trailing spaces. + temp.replace(QRegExp(" +\\t"), "\t"); + // Remove blank lines. + temp.replace(QRegExp("\t\t+"),"\t"); + // Split into sentences. + QStringList tempList = QStringList::split("\t", temp, false); + +// for ( QStringList::Iterator it = tempList.begin(); it != tempList.end(); ++it ) { +// kdDebug() << "'" << *it << "'" << endl; +// } + return tempList; +} + +/** +* Queues a text job. +*/ +uint SpeechData::setText( const QString &text, const QString &talker, const QCString &appId) +{ + // kdDebug() << "Running: SpeechData::setText" << endl; + mlJob* job = new mlJob; + ++jobCounter; + if (jobCounter == 0) ++jobCounter; // Overflow is OK, but don't want any 0 jobNums. + uint jobNum = jobCounter; + job->jobNum = jobNum; + job->appId = appId; + job->talker = talker; + job->state = KSpeech::jsQueued; + job->seq = 0; + job->partCount = 1; +#if NO_FILTERS + QStringList tempList = parseText(text, appId); + job->sentences = tempList; + job->partSeqNums.append(tempList.count()); + textJobs.append(job); + emit textSet(appId, jobNum); +#else + job->sentences = QStringList(); + job->partSeqNums = QValueList<int>(); + textJobs.append(job); + startJobFiltering(job, text, false); +#endif + return jobNum; +} + +/** +* Adds another part to a text job. Does not start speaking the text. +* (thread safe) +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application, +* but if no such job, applies to the last job queued by any application. +* @param text The message to be spoken. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* @return Part number for the added part. Parts are numbered starting at 1. +* +* The text is parsed into individual sentences. Call getTextCount to retrieve +* the sentence count. Call startText to mark the job as speakable and if the +* job is the first speakable job in the queue, speaking will begin. +* @see setText. +* @see startText. +*/ +int SpeechData::appendText(const QString &text, const uint jobNum, const QCString& /*appId*/) +{ + // kdDebug() << "Running: SpeechData::appendText" << endl; + int newPartNum = 0; + mlJob* job = findJobByJobNum(jobNum); + if (job) + { + job->partCount++; +#if NO_FILTERS + QStringList tempList = parseText(text, appId); + int sentenceCount = job->sentences.count(); + job->sentences += tempList; + job->partSeqNums.append(sentenceCount + tempList.count()); + newPartNum = job->partSeqNums.count() + 1; + emit textAppended(job->appId, jobNum, newPartNum); +#else + newPartNum = job->partSeqNums.count() + 1; + startJobFiltering(job, text, false); +#endif + } + return newPartNum; +} + +/** +* Given an appId, returns the last (most recently queued) job with that appId. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* @return Pointer to the text job. +* If no such job, returns 0. +* If appId is NULL, returns the last job in the queue. +* Does not change textJobs.current(). +*/ +mlJob* SpeechData::findLastJobByAppId(const QCString& appId) +{ + if (appId == NULL) + return textJobs.getLast(); + else + { + QPtrListIterator<mlJob> it(textJobs); + for (it.toLast() ; it.current(); --it ) + { + if (it.current()->appId == appId) + { + return it.current(); + } + } + return 0; + } +} + +/** +* Given an appId, returns the last (most recently queued) job with that appId, +* or if no such job, the last (most recent) job in the queue. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* @return Pointer to the text job. +* If no such job, returns 0. +* If appId is NULL, returns the last job in the queue. +* Does not change textJobs.current(). +*/ +mlJob* SpeechData::findAJobByAppId(const QCString& appId) +{ + mlJob* job = findLastJobByAppId(appId); + // if (!job) job = textJobs.getLast(); + return job; +} + +/** +* Given an appId, returns the last (most recently queued) Job Number with that appId, +* or if no such job, the Job Number of the last (most recent) job in the queue. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* @return Job Number of the text job. +* If no such job, returns 0. +* If appId is NULL, returns the Job Number of the last job in the queue. +* Does not change textJobs.current(). +*/ +uint SpeechData::findAJobNumByAppId(const QCString& appId) +{ + mlJob* job = findAJobByAppId(appId); + if (job) + return job->jobNum; + else + return 0; +} + +/** +* Given a jobNum, returns the first job with that jobNum. +* @return Pointer to the text job. +* If no such job, returns 0. +* Does not change textJobs.current(). +*/ +mlJob* SpeechData::findJobByJobNum(const uint jobNum) +{ + QPtrListIterator<mlJob> it(textJobs); + for ( ; it.current(); ++it ) + { + if (it.current()->jobNum == jobNum) + { + return it.current(); + } + } + return 0; +} + +/** +* Given a jobNum, returns the appId of the application that owns the job. +* @param jobNum Job number of the text job. +* @return appId of the job. +* If no such job, returns "". +* Does not change textJobs.current(). +*/ +QCString SpeechData::getAppIdByJobNum(const uint jobNum) +{ + QCString appId; + mlJob* job = findJobByJobNum(jobNum); + if (job) appId = job->appId; + return appId; +} + +/** +* Sets pointer to the TalkerMgr object. +*/ +void SpeechData::setTalkerMgr(TalkerMgr* talkerMgr) { m_talkerMgr = talkerMgr; } + +/** +* Remove a text job from the queue. +* (thread safe) +* @param jobNum Job number of the text job. +* +* The job is deleted from the queue and the textRemoved signal is emitted. +*/ +void SpeechData::removeText(const uint jobNum) +{ + // kdDebug() << "Running: SpeechData::removeText" << endl; + uint removeJobNum = 0; + QCString removeAppId; // The appId of the removed (and stopped) job. + mlJob* removeJob = findJobByJobNum(jobNum); + if (removeJob) + { + removeAppId = removeJob->appId; + removeJobNum = removeJob->jobNum; + // If filtering on the job, cancel it. + QPtrListIterator<PooledFilterMgr> it( m_pooledFilterMgrs ); + for ( ; it.current(); ++it ) { + PooledFilterMgr* pooledFilterMgr = it.current(); + if (pooledFilterMgr->job && (pooledFilterMgr->job->jobNum == removeJobNum)) + { + pooledFilterMgr->busy = false; + pooledFilterMgr->job = 0; + pooledFilterMgr->partNum = 0; + delete pooledFilterMgr->talkerCode; + pooledFilterMgr->talkerCode = 0; + pooledFilterMgr->filterMgr->stopFiltering(); + } + } + // Delete the job. + textJobs.removeRef(removeJob); + } + if (removeJobNum) emit textRemoved(removeAppId, removeJobNum); +} + +/** +* Given a job and a sequence number, returns the part that sentence is in. +* If no such job or sequence number, returns 0. +* @param job The text job. +* @param seq Sequence number of the sentence. Sequence numbers begin with 1. +* @return Part number of the part the sentence is in. Parts are numbered +* beginning with 1. If no such job or sentence, returns 0. +* +*/ +int SpeechData::getJobPartNumFromSeq(const mlJob& job, const int seq) +{ + int foundPartNum = 0; + int desiredSeq = seq; + uint partNum = 0; + // Wait until all filtering has stopped for the job. + waitJobFiltering(&job); + while (partNum < job.partSeqNums.count()) + { + if (desiredSeq <= job.partSeqNums[partNum]) + { + foundPartNum = partNum + 1; + break; + } + desiredSeq = desiredSeq - job.partSeqNums[partNum]; + ++partNum; + } + return foundPartNum; +} + + +/** +* Delete expired jobs. At most, one finished job is kept on the queue. +* @param finishedJobNum Job number of a job that just finished. +* The just finished job is not deleted, but any other finished jobs are. +* Does not change the textJobs.current() pointer. +*/ +void SpeechData::deleteExpiredJobs(const uint finishedJobNum) +{ + // Save current pointer. + typedef QPair<QCString, uint> removedJob; + typedef QValueList<removedJob> removedJobsList; + removedJobsList removedJobs; + // Walk through jobs and delete any other finished jobs. + for (mlJob* job = textJobs.first(); (job); job = textJobs.next()) + { + if (job->jobNum != finishedJobNum && job->state == KSpeech::jsFinished) + { + removedJobs.append(removedJob(job->appId, job->jobNum)); + textJobs.removeRef(job); + } + } + // Emit signals for removed jobs. + removedJobsList::const_iterator it; + removedJobsList::const_iterator endRemovedJobsList(removedJobs.constEnd()); + for (it = removedJobs.constBegin(); it != endRemovedJobsList ; ++it) + { + QCString appId = (*it).first; + uint jobNum = (*it).second; + textRemoved(appId, jobNum); + } +} + +/** +* Given a Job Number, returns the next speakable text job on the queue. +* @param prevJobNum Current job number (which should not be returned). +* @return Pointer to mlJob structure of the first speakable job +* not equal prevJobNum. If no such job, returns null. +* +* Caller must not delete the job. +*/ +mlJob* SpeechData::getNextSpeakableJob(const uint prevJobNum) +{ + for (mlJob* job = textJobs.first(); (job); job = textJobs.next()) + if (job->jobNum != prevJobNum) + if (job->state == KSpeech::jsSpeakable) + { + waitJobFiltering(job); + return job; + } + return 0; +} + +/** +* Given previous job number and sequence number, returns the next sentence from the +* text queue. If no such sentence is available, either because we've run out of +* jobs, or because all jobs are paused, returns null. +* @param prevJobNum Previous Job Number. +* @param prevSeq Previous sequency number. +* @return Pointer to n mlText structure containing the next sentence. If no +* sentence, returns null. +* +* Caller is responsible for deleting the returned mlText structure (if not null). +*/ +mlText* SpeechData::getNextSentenceText(const uint prevJobNum, const uint prevSeq) +{ + // kdDebug() << "SpeechData::getNextSentenceText running with prevJobNum " << prevJobNum << " prevSeq " << prevSeq << endl; + mlText* temp = 0; + uint jobNum = prevJobNum; + mlJob* job = 0; + uint seq = prevSeq; + ++seq; + if (!jobNum) + { + job = getNextSpeakableJob(jobNum); + if (job) seq =+ job->seq; + } else + job = findJobByJobNum(prevJobNum); + if (!job) + { + job = getNextSpeakableJob(jobNum); + if (job) seq =+ job->seq; + } + else + { + if ((job->state != KSpeech::jsSpeakable) && (job->state != KSpeech::jsSpeaking)) + { + job = getNextSpeakableJob(job->jobNum); + if (job) seq =+ job->seq; + } + } + if (job) + { + // If we run out of sentences in the job, move on to next job. + jobNum = job->jobNum; + if (seq > job->sentences.count()) + { + job = getNextSpeakableJob(jobNum); + if (job) seq =+ job->seq; + } + } + if (job) + { + if (seq == 0) seq = 1; + temp = new mlText; + temp->text = job->sentences[seq - 1]; + temp->appId = job->appId; + temp->talker = job->talker; + temp->jobNum = job->jobNum; + temp->seq = seq; + // kdDebug() << "SpeechData::getNextSentenceText: return job number " << temp->jobNum << " seq " << temp->seq << " sentence count = " << job->sentences.count() << endl; + } // else kdDebug() << "SpeechData::getNextSentenceText: no more sentences in queue" << endl; + return temp; +} + +/** +* Given a Job Number, sets the current sequence number of the job. +* @param jobNum Job Number. +* @param seq Sequence number. +* If for some reason, the job does not exist, nothing happens. +*/ +void SpeechData::setJobSequenceNum(const uint jobNum, const uint seq) +{ + mlJob* job = findJobByJobNum(jobNum); + if (job) job->seq = seq; +} + +/** +* Given a Job Number, returns the current sequence number of the job. +* @param jobNum Job Number. +* @return Sequence number of the job. If no such job, returns 0. +*/ +uint SpeechData::getJobSequenceNum(const uint jobNum) +{ + mlJob* job = findJobByJobNum(jobNum); + if (job) + return job->seq; + else + return 0; +} + +/** +* Sets the GREP pattern that will be used as the sentence delimiter. +* @param delimiter A valid GREP pattern. +* @param appId The DCOP senderId of the application. NULL if kttsd. +* +* The default delimiter is + @verbatim + ([\\.\\?\\!\\:\\;])\\s + @endverbatim +* +* Note that backward slashes must be escaped. +* +* Changing the sentence delimiter does not affect other applications. +* @see sentenceparsing +*/ +void SpeechData::setSentenceDelimiter(const QString &delimiter, const QCString appId) +{ + sentenceDelimiters[appId] = delimiter; +} + +/** +* Get the number of sentences in a text job. +* (thread safe) +* @param jobNum Job number of the text job. +* @return The number of sentences in the job. -1 if no such job. +* +* The sentences of a job are given sequence numbers from 1 to the number returned by this +* method. The sequence numbers are emitted in the sentenceStarted and sentenceFinished signals. +*/ +int SpeechData::getTextCount(const uint jobNum) +{ + mlJob* job = findJobByJobNum(jobNum); + int temp; + if (job) + { + waitJobFiltering(job); + temp = job->sentences.count(); + } else + temp = -1; + return temp; +} + +/** +* Get the number of jobs in the text job queue. +* (thread safe) +* @return Number of text jobs in the queue. 0 if none. +*/ +uint SpeechData::getTextJobCount() +{ + return textJobs.count(); +} + +/** +* Get a comma-separated list of text job numbers in the queue. +* @return Comma-separated list of text job numbers in the queue. +*/ +QString SpeechData::getTextJobNumbers() +{ + QString jobs; + QPtrListIterator<mlJob> it(textJobs); + for ( ; it.current(); ++it ) + { + if (!jobs.isEmpty()) jobs.append(","); + jobs.append(QString::number(it.current()->jobNum)); + } + return jobs; +} + +/** +* Get the state of a text job. +* (thread safe) +* @param jobNum Job number of the text job. +* @return State of the job. -1 if invalid job number. +*/ +int SpeechData::getTextJobState(const uint jobNum) +{ + mlJob* job = findJobByJobNum(jobNum); + int temp; + if (job) + temp = job->state; + else + temp = -1; + return temp; +} + +/** +* Set the state of a text job. +* @param jobNum Job Number of the job. +* @param state New state for the job. +* +* If the new state is Finished, deletes other expired jobs. +* +**/ +void SpeechData::setTextJobState(const uint jobNum, const KSpeech::kttsdJobState state) +{ + mlJob* job = findJobByJobNum(jobNum); + if (job) + { + job->state = state; + if (state == KSpeech::jsFinished) deleteExpiredJobs(jobNum); + } +} + +/** +* Get information about a text job. +* @param jobNum Job number of the text job. +* @return A QDataStream containing information about the job. +* Blank if no such job. +* +* The stream contains the following elements: +* - int state Job state. +* - QCString appId DCOP senderId of the application that requested the speech job. +* - QString talker Language code in which to speak the text. +* - int seq Current sentence being spoken. Sentences are numbered starting at 1. +* - int sentenceCount Total number of sentences in the job. +* - int partNum Current part of the job begin spoken. Parts are numbered starting at 1. +* - int partCount Total number of parts in the job. +* +* Note that sequence numbers apply to the entire job. +* They do not start from 1 at the beginning of each part. +* +* The following sample code will decode the stream: + @verbatim + QByteArray jobInfo = getTextJobInfo(jobNum); + QDataStream stream(jobInfo, IO_ReadOnly); + int state; + QCString appId; + QString talker; + int seq; + int sentenceCount; + int partNum; + int partCount; + stream >> state; + stream >> appId; + stream >> talker; + stream >> seq; + stream >> sentenceCount; + stream >> partNum; + stream >> partCount; + @endverbatim +*/ +QByteArray SpeechData::getTextJobInfo(const uint jobNum) +{ + mlJob* job = findJobByJobNum(jobNum); + QByteArray temp; + if (job) + { + waitJobFiltering(job); + QDataStream stream(temp, IO_WriteOnly); + stream << job->state; + stream << job->appId; + stream << job->talker; + stream << job->seq; + stream << job->sentences.count(); + stream << getJobPartNumFromSeq(*job, job->seq); + stream << job->partSeqNums.count(); + } + return temp; +} + +/** +* Return a sentence of a job. +* @param jobNum Job number of the text job. +* @param seq Sequence number of the sentence. +* @return The specified sentence in the specified job. If no such +* job or sentence, returns "". +*/ +QString SpeechData::getTextJobSentence(const uint jobNum, const uint seq /*=1*/) +{ + mlJob* job = findJobByJobNum(jobNum); + QString temp; + if (job) + { + waitJobFiltering(job); + temp = job->sentences[seq - 1]; + } + return temp; +} + +/** +* Change the talker for a text job. +* @param jobNum Job number of the text job. +* If zero, applies to the last job queued by the application, +* but if no such job, applies to the last job queued by any application. +* @param talker New code for the talker to do the speaking. Example "en". +* If NULL, defaults to the user's default talker. +* If no plugin has been configured for the specified Talker code, +* defaults to the closest matching talker. +*/ +void SpeechData::changeTextTalker(const QString &talker, uint jobNum) +{ + mlJob* job = findJobByJobNum(jobNum); + if (job) job->talker = talker; +} + +/** +* Move a text job down in the queue so that it is spoken later. +* @param jobNum Job number of the text job. +*/ +void SpeechData::moveTextLater(const uint jobNum) +{ + // kdDebug() << "Running: SpeechData::moveTextLater" << endl; + mlJob* job = findJobByJobNum(jobNum); + if (job) + { + // Get index of the job. + uint index = textJobs.findRef(job); + // Move job down one position in the queue. + // kdDebug() << "In SpeechData::moveTextLater, moving jobNum " << movedJobNum << endl; + if (textJobs.insert(index + 2, job)) textJobs.take(index); + } +} + +/** +* Jump to the first sentence of a specified part of a text job. +* @param partNum Part number of the part to jump to. Parts are numbered starting at 1. +* @param jobNum Job number of the text job. +* @return Part number of the part actually jumped to. +* +* If partNum is greater than the number of parts in the job, jumps to last part. +* If partNum is 0, does nothing and returns the current part number. +* If no such job, does nothing and returns 0. +* Does not affect the current speaking/not-speaking state of the job. +*/ +int SpeechData::jumpToTextPart(const int partNum, const uint jobNum) +{ + // kdDebug() << "Running: SpeechData::jumpToTextPart" << endl; + int newPartNum = 0; + mlJob* job = findJobByJobNum(jobNum); + if (job) + { + waitJobFiltering(job); + if (partNum > 0) + { + newPartNum = partNum; + int partCount = job->partSeqNums.count(); + if (newPartNum > partCount) newPartNum = partCount; + if (newPartNum > 1) + job->seq = job->partSeqNums[newPartNum - 1]; + else + job->seq = 0; + } + else + newPartNum = getJobPartNumFromSeq(*job, job->seq); + } + return newPartNum; +} + +/** +* Advance or rewind N sentences in a text job. +* @param n Number of sentences to advance (positive) or rewind (negative) +* in the job. +* @param jobNum Job number of the text job. +* @return Sequence number of the sentence actually moved to. Sequence numbers +* are numbered starting at 1. +* +* If no such job, does nothing and returns 0. +* If n is zero, returns the current sequence number of the job. +* Does not affect the current speaking/not-speaking state of the job. +*/ +uint SpeechData::moveRelTextSentence(const int n, const uint jobNum /*=0*/) +{ + // kdDebug() << "Running: SpeechData::moveRelTextSentence" << endl; + int newSeqNum = 0; + mlJob* job = findJobByJobNum(jobNum); + if (job) + { + waitJobFiltering(job); + int oldSeqNum = job->seq; + newSeqNum = oldSeqNum + n; + if (n != 0) + { + if (newSeqNum < 0) newSeqNum = 0; + int sentenceCount = job->sentences.count(); + if (newSeqNum > sentenceCount) newSeqNum = sentenceCount; + job->seq = newSeqNum; + } + } + return newSeqNum; +} + +/** +* Assigns a FilterMgr to a job and starts filtering on it. +*/ +void SpeechData::startJobFiltering(mlJob* job, const QString& text, bool noSBD) +{ + uint jobNum = job->jobNum; + int partNum = job->partCount; + // kdDebug() << "SpeechData::startJobFiltering: jobNum = " << jobNum << " partNum = " << partNum << " text.left(500) = " << text.left(500) << endl; + // Find an idle FilterMgr, if any. + // If filtering is already in progress for this job and part, do nothing. + PooledFilterMgr* pooledFilterMgr = 0; + QPtrListIterator<PooledFilterMgr> it( m_pooledFilterMgrs ); + for( ; it.current(); ++it ) + { + if (it.current()->busy) { + if ((it.current()->job->jobNum == jobNum) && (it.current()->partNum == partNum)) return; + } else { + if (!it.current()->job && !pooledFilterMgr) pooledFilterMgr = it.current(); + } + } + // Create a new FilterMgr if needed and add to pool. + if (!pooledFilterMgr) + { + // kdDebug() << "SpeechData::startJobFiltering: adding new pooledFilterMgr for job " << jobNum << " part " << partNum << endl; + pooledFilterMgr = new PooledFilterMgr(); + FilterMgr* filterMgr = new FilterMgr(); + filterMgr->init(config, "General"); + pooledFilterMgr->filterMgr = filterMgr; + // Connect signals from FilterMgr. + connect (filterMgr, SIGNAL(filteringFinished()), this, SLOT(slotFilterMgrFinished())); + connect (filterMgr, SIGNAL(filteringStopped()), this, SLOT(slotFilterMgrStopped())); + m_pooledFilterMgrs.append(pooledFilterMgr); + } + // else kdDebug() << "SpeechData::startJobFiltering: re-using idle pooledFilterMgr for job " << jobNum << " part " << partNum << endl; + // Flag the FilterMgr as busy and set it going. + pooledFilterMgr->busy = true; + pooledFilterMgr->job = job; + pooledFilterMgr->partNum = partNum; + pooledFilterMgr->filterMgr->setNoSBD( noSBD ); + // Get TalkerCode structure of closest matching Talker. + pooledFilterMgr->talkerCode = m_talkerMgr->talkerToTalkerCode(job->talker); + // Pass Sentence Boundary regular expression (if app overrode default); + if (sentenceDelimiters.find(job->appId) != sentenceDelimiters.end()) + pooledFilterMgr->filterMgr->setSbRegExp(sentenceDelimiters[job->appId]); + pooledFilterMgr->filterMgr->asyncConvert(text, pooledFilterMgr->talkerCode, job->appId); +} + +/** +* Waits for filtering to be completed on a job. +* This is typically called because an app has requested job info that requires +* filtering to be completed, such as getJobInfo. +*/ +void SpeechData::waitJobFiltering(const mlJob* job) +{ +#if NO_FILTERS + return; +#endif + uint jobNum = job->jobNum; + bool waited = false; + QPtrListIterator<PooledFilterMgr> it(m_pooledFilterMgrs); + for ( ; it.current(); ++it ) + { + PooledFilterMgr* pooledFilterMgr = it.current(); + if (pooledFilterMgr->busy) + { + if (pooledFilterMgr->job->jobNum == jobNum) + { + if (!pooledFilterMgr->filterMgr->noSBD()) + kdDebug() << "SpeechData::waitJobFiltering: Waiting for filter to finish. Not optimium. " << + "Try waiting for textSet signal before querying for job information." << endl; + pooledFilterMgr->filterMgr->waitForFinished(); + // kdDebug() << "SpeechData::waitJobFiltering: waiting for job " << jobNum << endl; + waited = true; + } + } + } + if (waited) + doFiltering(); +} + +/** +* Processes filters by looping across the pool of FilterMgrs. +* As each FilterMgr finishes, emits appropriate signals and flags it as no longer busy. +*/ +void SpeechData::doFiltering() +{ + // kdDebug() << "SpeechData::doFiltering: Running. " << m_pooledFilterMgrs.count() << " filters in pool." << endl; + bool again = true; + while (again) + { + again = false; + QPtrListIterator<PooledFilterMgr> it( m_pooledFilterMgrs ); + for( ; it.current(); ++it ) + { + PooledFilterMgr* pooledFilterMgr = it.current(); + // If FilterMgr is busy, see if it is now finished. + if (pooledFilterMgr->busy) + { + FilterMgr* filterMgr = pooledFilterMgr->filterMgr; + if (filterMgr->getState() == FilterMgr::fsFinished) + { + mlJob* job = pooledFilterMgr->job; + // kdDebug() << "SpeechData::doFiltering: filter finished, jobNum = " << job->jobNum << " partNum = " << pooledFilterMgr->partNum << endl; + // We have to retrieve parts in order, but parts may not be completed in order. + // See if this is the next part we need. + if ((int)job->partSeqNums.count() == (pooledFilterMgr->partNum - 1)) + { + pooledFilterMgr->busy = false; + // Retrieve text from FilterMgr. + QString text = filterMgr->getOutput(); + // kdDebug() << "SpeechData::doFiltering: text.left(500) = " << text.left(500) << endl; + filterMgr->ackFinished(); + // Convert the TalkerCode back into string. + job->talker = pooledFilterMgr->talkerCode->getTalkerCode(); + // TalkerCode object no longer needed. + delete pooledFilterMgr->talkerCode; + pooledFilterMgr->talkerCode = 0; + if (filterMgr->noSBD()) + job->sentences = text; + else + { + // Split the text into sentences and store in the job. + // The SBD plugin does all the real sentence parsing, inserting tabs at each + // sentence boundary. + QStringList sentences = QStringList::split("\t", text, false); + int sentenceCount = job->sentences.count(); + job->sentences += sentences; + job->partSeqNums.append(sentenceCount + sentences.count()); + } + int partNum = job->partSeqNums.count(); + // Clean up. + pooledFilterMgr->job = 0; + pooledFilterMgr->partNum = 0; + // Emit signal. + if (!filterMgr->noSBD()) + { + if (partNum == 1) + emit textSet(job->appId, job->jobNum); + else + emit textAppended(job->appId, job->jobNum, partNum); + } + } else { + // A part is ready, but need to first process a finished preceeding part + // that follows this one in the pool of filter managers. + again = true; + // kdDebug() << "SpeechData::doFiltering: filter is finished, but must wait for earlier part to finish filter, job = " << pooledFilterMgr->job->jobNum << endl; + } + } + // else kdDebug() << "SpeechData::doFiltering: filter for job " << pooledFilterMgr->job->jobNum << " is busy." << endl; + } + // else kdDebug() << "SpeechData::doFiltering: filter is idle" << endl; + } + } +} + +void SpeechData::slotFilterMgrFinished() +{ + // kdDebug() << "SpeechData::slotFilterMgrFinished: received signal FilterMgr finished signal." << endl; + doFiltering(); +} + +void SpeechData::slotFilterMgrStopped() +{ + doFiltering(); +} + diff --git a/kttsd/kttsd/speechdata.h b/kttsd/kttsd/speechdata.h new file mode 100644 index 0000000..40294bf --- /dev/null +++ b/kttsd/kttsd/speechdata.h @@ -0,0 +1,731 @@ +/*************************************************** vim:set ts=4 sw=4 sts=4: + This contains the SpeechData class which is in charge of maintaining + all the data on the memory. + It maintains queues manages the text. + We could say that this is the common repository between the KTTSD class + (dcop service) and the Speaker class (speaker, loads plug ins, call plug in + functions) + ------------------- + Copyright: + (C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org> + (C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org> + (C) 2004-2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: José Pablo Ezequiel "Pupeno" Fernández + ******************************************************************************/ + +/****************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License. * + * * + ******************************************************************************/ + +#ifndef _SPEECHDATA_H_ +#define _SPEECHDATA_H_ + +// Qt includes. +#include <qptrqueue.h> +#include <qptrlist.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qmap.h> + +// KDE includes. +#include <kconfig.h> + +// KTTS includes. +#include <kspeech.h> +#include <talkercode.h> +#include <filtermgr.h> + +class TalkerMgr; + +/** +* Struct containing a text cell, for messages, warnings, and texts. +* Contains the text itself, the associated talker, +* the ID of the application that requested it be spoken, and a sequence number. +*/ +struct mlText{ + QString talker; /* Requested Talker code for the sentence. */ + QString text; /* Text of sentence. */ + QCString appId; /* DCOP senderId of the application that requested the speech. */ + uint jobNum; /* Text jobNum. Only applies to text messages; not warning and messages. */ + uint seq; /* Sequence number. */ +}; + +/** + * Struct containing a text job. + */ +struct mlJob { + uint jobNum; /* Job number. */ + KSpeech::kttsdJobState state; /* Job state. */ + QCString appId; /* DCOP senderId of the application that requested the speech job. */ + QString talker; /* Requested Talker code in which to speak the text. */ + int seq; /* Current sentence being spoken. */ + QValueList<int> partSeqNums; /* List containing last sequence number for each part of a job. */ + QStringList sentences; /* List of sentences in the job. */ + int partCount; /* Number of parts in the job. */ +}; + +/** + * Struct used to keep a pool of FilterMgr objects. + */ +struct PooledFilterMgr { + FilterMgr* filterMgr; /* The FilterMgr object. */ + bool busy; /* True if the FilterMgr is busy. */ + mlJob* job; /* The job the FilterMgr is filtering. */ + int partNum; /* The part number of the job that is filtering. */ + TalkerCode* talkerCode; /* TalkerCode object passed to FilterMgr. */ +}; + +/** + * Struct used to keep notification options. + */ +struct NotifyOptions { + QString eventName; + int action; + QString talker; + QString customMsg; +}; + +/** + * A list of notification options for a single app, indexed by event. + */ +typedef QMap<QString, NotifyOptions> NotifyEventMap; + +/** + * A list of notification event maps for all apps, indexed by app. + */ +typedef QMap<QString, NotifyEventMap> NotifyAppMap; + +/** + * SpeechData class which is in charge of maintaining all the data on the memory. + * It maintains queues and has methods to enque + * messages and warnings and manage the text queues. + * We could say that this is the common repository between the KTTSD class + * (dcop service) and the Speaker class (speaker, loads plug ins, call plug in + * functions) + */ +class SpeechData : public QObject { + Q_OBJECT + + public: + /** + * Constructor + * Sets text to be stopped and warnings and messages queues to be autodelete (thread safe) + */ + SpeechData(); + + /** + * Destructor + */ + ~SpeechData(); + + /** + * Read the configuration + */ + bool readConfig(); + + /** + * Say a message as soon as possible, interrupting any other speech in progress. + * IMPORTANT: This method is reserved for use by Screen Readers and should not be used + * by any other applications. + * @param msg The message to be spoken. + * @param talker Code for the talker to speak the message. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + * @param appId The DCOP senderId of the application. + * + * If an existing Screen Reader output is in progress, it is stopped and discarded and + * replaced with this new message. + */ + void setScreenReaderOutput(const QString &msg, const QString &talker, + const QCString& appId); + + /** + * Given an appId, returns the last (most recently queued) Job Number with that appId, + * or if no such job, the Job Number of the last (most recent) job in the queue. + * @param appId The DCOP senderId of the application. + * @return Job Number of the text job. + * If no such job, returns 0. + * If appId is NULL, returns the Job Number of the last job in the queue. + * Does not change textJobs.current(). + */ + uint findAJobNumByAppId(const QCString& appId); + + /** + * Retrieves the Screen Reader Output. + */ + mlText* getScreenReaderOutput(); + + /** + * Returns true if Screen Reader Output is ready to be spoken. + */ + bool screenReaderOutputReady(); + + /** + * Add a new warning to the queue. + */ + void enqueueWarning( const QString &, const QString &talker, + const QCString& appId); + + /** + * Pop (get and erase) a warning from the queue. + * @return Pointer to mlText structure containing the warning. + * + * Caller is responsible for deleting the structure. + */ + mlText* dequeueWarning(); + + /** + * Are there any Warnings? + */ + bool warningInQueue(); + + /** + * Add a new message to the queue. + */ + void enqueueMessage( const QString &, const QString &talker, + const QCString&); + + /** + * Pop (get and erase) a message from the queue. + * @return Pointer to mlText structure containing the message. + * + * Caller is responsible for deleting the structure. + */ + mlText* dequeueMessage(); + + /** + * Are there any Messages? + */ + bool messageInQueue(); + + /** + * Sets the GREP pattern that will be used as the sentence delimiter. + * @param delimiter A valid GREP pattern. + * @param appId The DCOP senderId of the application. + * + * The default delimiter is + @verbatim + ([\\.\\?\\!\\:\\;])\\s + @endverbatim + * + * Note that backward slashes must be escaped. + * + * Changing the sentence delimiter does not affect other applications. + * @see sentenceparsing + */ + void setSentenceDelimiter(const QString &delimiter, const QCString appId); + + /* The following methods correspond to the methods in KSpeech interface. */ + + /** + * Queue a text job. Does not start speaking the text. + * (thread safe) + * @param text The message to be spoken. + * @param talker Code for the talker to speak the text. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + * @param appId The DCOP senderId of the application. + * @return Job number. + * + * The text is parsed into individual sentences. Call getTextCount to retrieve + * the sentence count. Call startText to mark the job as speakable and if the + * job is the first speakable job in the queue, speaking will begin. + * @see startText. + */ + uint setText(const QString &text, const QString &talker, const QCString& appId); + + /** + * Adds another part to a text job. Does not start speaking the text. + * (thread safe) + * @param jobNum Job number of the text job. + * @param text The message to be spoken. + * @param appId The DCOP senderId of the application. + * @return Part number for the added part. Parts are numbered starting at 1. + * + * The text is parsed into individual sentences. Call getTextCount to retrieve + * the sentence count. Call startText to mark the job as speakable and if the + * job is the first speakable job in the queue, speaking will begin. + * @see setText. + * @see startText. + */ + int appendText(const QString &text, const uint jobNum, const QCString& appId); + + /** + * Get the number of sentences in a text job. + * (thread safe) + * @param jobNum Job number of the text job. + * @return The number of sentences in the job. -1 if no such job. + * + * The sentences of a job are given sequence numbers from 1 to the number returned by this + * method. The sequence numbers are emitted in the sentenceStarted and sentenceFinished signals. + */ + int getTextCount(const uint jobNum); + + /** + * Get the number of jobs in the text job queue. + * (thread safe) + * @return Number of text jobs in the queue. 0 if none. + */ + uint getTextJobCount(); + + /** + * Get a comma-separated list of text job numbers in the queue. + * @return Comma-separated list of text job numbers in the queue. + */ + QString getTextJobNumbers(); + + /** + * Get the state of a text job. + * (thread safe) + * @param jobNum Job number of the text job. + * @return State of the job. -1 if invalid job number. + */ + int getTextJobState(const uint jobNum); + + /** + * Set the state of a text job. + * @param jobNum Job Number of the job. + * @param state New state for the job. + * + **/ + void setTextJobState(const uint jobNum, const KSpeech::kttsdJobState state); + + /** + * Get information about a text job. + * @param jobNum Job number of the text job. + * @return A QDataStream containing information about the job. + * Blank if no such job. + * + * The stream contains the following elements: + * - int state Job state. + * - QCString appId DCOP senderId of the application that requested the speech job. + * - QString talker Talker code as requested by application. + * - int seq Current sentence being spoken. Sentences are numbered starting at 1. + * - int sentenceCount Total number of sentences in the job. + * - int partNum Current part of the job begin spoken. Parts are numbered starting at 1. + * - int partCount Total number of parts in the job. + * + * Note that sequence numbers apply to the entire job. + * They do not start from 1 at the beginning of each part. + * + * The following sample code will decode the stream: + @verbatim + QByteArray jobInfo = getTextJobInfo(jobNum); + QDataStream stream(jobInfo, IO_ReadOnly); + int state; + QCString appId; + QString talker; + int seq; + int sentenceCount; + int partNum; + int partCount; + stream >> state; + stream >> appId; + stream >> talker; + stream >> seq; + stream >> sentenceCount; + stream >> partNum; + stream >> partCount; + @endverbatim + */ + QByteArray getTextJobInfo(const uint jobNum); + + /** + * Return a sentence of a job. + * @param jobNum Job number of the text job. + * @param seq Sequence number of the sentence. + * @return The specified sentence in the specified job. If no such + * job or sentence, returns "". + */ + QString getTextJobSentence(const uint jobNum, const uint seq=1); + + /** + * Remove a text job from the queue. + * (thread safe) + * @param jobNum Job number of the text job. + * + * The job is deleted from the queue and the textRemoved signal is emitted. + */ + void removeText(const uint jobNum); + + /** + * Change the talker for a text job. + * @param jobNum Job number of the text job. + * @param talker New code for the talker to do speaking. Example "en". + * If NULL, defaults to the user's default talker. + * If no plugin has been configured for the specified Talker code, + * defaults to the closest matching talker. + */ + void changeTextTalker(const QString &talker, uint jobNum); + + /** + * Move a text job down in the queue so that it is spoken later. + * @param jobNum Job number of the text job. + */ + void moveTextLater(const uint jobNum); + + /** + * Jump to the first sentence of a specified part of a text job. + * @param partNum Part number of the part to jump to. Parts are numbered starting at 1. + * @param jobNum Job number of the text job. + * @return Part number of the part actually jumped to. + * + * If partNum is greater than the number of parts in the job, jumps to last part. + * If partNum is 0, does nothing and returns the current part number. + * If no such job, does nothing and returns 0. + * Does not affect the current speaking/not-speaking state of the job. + */ + int jumpToTextPart(const int partNum, const uint jobNum); + + /** + * Advance or rewind N sentences in a text job. + * @param n Number of sentences to advance (positive) or rewind (negative) + * in the job. + * @param jobNum Job number of the text job. + * @return Sequence number of the sentence actually moved to. Sequence numbers + * are numbered starting at 1. + * + * If no such job, does nothing and returns 0. + * If n is zero, returns the current sequence number of the job. + * Does not affect the current speaking/not-speaking state of the job. + */ + uint moveRelTextSentence(const int n, const uint jobNum); + + /** + * Given a jobNum, returns the first job with that jobNum. + * @return Pointer to the text job. + * If no such job, returns 0. + * Does not change textJobs.current(). + */ + mlJob* findJobByJobNum(const uint jobNum); + + /** + * Given a Job Number, returns the next speakable text job on the queue. + * @param prevJobNum Current job number (which should not be returned). + * @return Pointer to mlJob structure of the first speakable job + * not equal prevJobNum. If no such job, returns null. + * + * Caller must not delete the job. + */ + mlJob* getNextSpeakableJob(const uint prevJobNum); + + /** + * Given previous job number and sequence number, returns the next sentence from the + * text queue. If no such sentence is available, either because we've run out of + * jobs, or because all jobs are paused, returns null. + * @param prevJobNum Previous Job Number. + * @param prevSeq Previous sequency number. + * @return Pointer to n mlText structure containing the next sentence. If no + * sentence, returns null. + * + * Caller is responsible for deleting the returned mlText structure (if not null). + */ + mlText* getNextSentenceText(const uint prevJobNum, const uint prevSeq); + + /** + * Given a Job Number, sets the current sequence number of the job. + * @param jobNum Job Number. + * @param seq Sequence number. + * If for some reason, the job does not exist, nothing happens. + */ + void setJobSequenceNum(const uint jobNum, const uint seq); + + /** + * Given a Job Number, returns the current sequence number of the job. + * @param jobNum Job Number. + * @return Sequence number of the job. If no such job, returns 0. + */ + uint getJobSequenceNum(const uint jobNum); + + /** + * Given a jobNum, returns the appId of the application that owns the job. + * @param jobNum Job number of the text job. + * @return appId of the job. + * If no such job, returns "". + * Does not change textJobs.current(). + */ + QCString getAppIdByJobNum(const uint jobNum); + + /** + * Sets pointer to the TalkerMgr object. + */ + void setTalkerMgr(TalkerMgr* talkerMgr); + + /* The following properties come from the configuration. */ + + /** + * Text pre message + */ + QString textPreMsg; + + /** + * Text pre message enabled ? + */ + bool textPreMsgEnabled; + + /** + * Text pre sound + */ + QString textPreSnd; + + /** + * Text pre sound enabled ? + */ + bool textPreSndEnabled; + + /** + * Text post message + */ + QString textPostMsg; + + /** + * Text post message enabled ? + */ + bool textPostMsgEnabled; + + /** + * Text post sound + */ + QString textPostSnd; + + /** + * Text post sound enabled ? + */ + bool textPostSndEnabled; + + /** + * Paragraph pre message + */ + QString parPreMsg; + + /** + * Paragraph pre message enabled ? + */ + bool parPreMsgEnabled; + + /** + * Paragraph pre sound + */ + QString parPreSnd; + + /** + * Paragraph pre sound enabled ? + */ + bool parPreSndEnabled; + + /** + * Paragraph post message + */ + QString parPostMsg; + + /** + * Paragraph post message enabled ? + */ + bool parPostMsgEnabled; + + /** + * Paragraph post sound + */ + QString parPostSnd; + + /** + * Paragraph post sound enabled ? + */ + bool parPostSndEnabled; + + /** + * Keep audio files. Do not delete generated tmp wav files. + */ + bool keepAudio; + QString keepAudioPath; + + /** + * Notification settings. + */ + bool notify; + bool notifyExcludeEventsWithSound; + NotifyAppMap notifyAppMap; + int notifyDefaultPresent; + NotifyOptions notifyDefaultOptions; + + /** + * Automatically start KTTSMgr whenever speaking. + */ + bool autoStartManager; + + /** + * Automatically exit auto-started KTTSMgr when speaking finishes. + */ + bool autoExitManager; + + /** + * Configuration + */ + KConfig *config; + + /** + * True if at least one XML Transformer plugin for html is enabled. + */ + bool supportsHTML; + + signals: + /** + * This signal is emitted whenever a new text job is added to the queue. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + */ + void textSet(const QCString& appId, const uint jobNum); + + /** + * This signal is emitted whenever a new part is appended to a text job. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + * @param partNum Part number of the new part. Parts are numbered starting + * at 1. + */ + void textAppended(const QCString& appId, const uint jobNum, const int partNum); + + /** + * This signal is emitted whenever a text job is deleted from the queue. + * The job is no longer in the queue when this signal is emitted. + * @param appId The DCOP senderId of the application that created the job. + * @param jobNum Job number of the text job. + */ + void textRemoved(const QCString& appId, const uint jobNum); + + private: + /** + * Screen Reader Output. + */ + mlText screenReaderOutput; + + /** + * Queue of warnings + */ + QPtrQueue<mlJob> warnings; + + /** + * Queue of messages + */ + QPtrQueue<mlJob> messages; + + /** + * Queue of text jobs. + */ + QPtrList<mlJob> textJobs; + + /** + * TalkerMgr object local pointer. + */ + TalkerMgr* m_talkerMgr; + + /** + * Pool of FilterMgrs. + */ + QPtrList<PooledFilterMgr> m_pooledFilterMgrs; + + /** + * Job counter. Each new job increments this counter. + */ + uint jobCounter; + + /** + * Talker of the text + */ + QString textTalker; + + /** + * Map of sentence delimiters. One per app. If none specified for an app, uses default. + */ + QMap<QCString, QString> sentenceDelimiters; + + /** + * Determines whether the given text is SSML markup. + */ + bool isSsml(const QString &text); + + /** + * Given an appId, returns the last (most recently queued) job with that appId. + * @param appId The DCOP senderId of the application. + * @return Pointer to the text job. + * If no such job, returns 0. + * If appId is NULL, returns the last job in the queue. + * Does not change textJobs.current(). + */ + mlJob* findLastJobByAppId(const QCString& appId); + + /** + * Given an appId, returns the last (most recently queued) job with that appId, + * or if no such job, the last (most recent) job in the queue. + * @param appId The DCOP senderId of the application. + * @return Pointer to the text job. + * If no such job, returns 0. + * If appId is NULL, returns the last job in the queue. + * Does not change textJobs.current(). + */ + mlJob* findAJobByAppId(const QCString& appId); + + /** + * Given a job and a sequence number, returns the part that sentence is in. + * If no such job or sequence number, returns 0. + * @param job The text job. + * @param seq Sequence number of the sentence. Sequence numbers begin with 1. + * @return Part number of the part the sentence is in. Parts are numbered + * beginning with 1. If no such job or sentence, returns 0. + */ + int getJobPartNumFromSeq(const mlJob& job, const int seq); + + /** + * Parses a block of text into sentences using the application-specified regular expression + * or (if not specified), the default regular expression. + * @param text The message to be spoken. + * @param appId The DCOP senderId of the application. + * @return List of parsed sentences. + */ + + QStringList parseText(const QString &text, const QCString &appId); + + /** + * Delete expired jobs. At most, one finished job is kept on the queue. + * @param finishedJobNum Job number of a job that just finished + * The just finished job is not deleted, but any other finished jobs are. + * Does not change the textJobs.current() pointer. + */ + void deleteExpiredJobs(const uint finishedJobNum); + + /** + * Assigns a FilterMgr to a job and starts filtering on it. + */ + void startJobFiltering(mlJob* job, const QString& text, bool noSBD); + + /** + * Waits for filtering to be completed on a job. + * This is typically called because an app has requested job info that requires + * filtering to be completed, such as getJobInfo. + */ + void waitJobFiltering(const mlJob* job); + + /** + * Processes filters by looping across the pool of FilterMgrs. + * As each FilterMgr finishes, emits appropriate signals and flags it as no longer busy. + */ + void doFiltering(); + + /** + * Loads notify events from a file. Clearing data if clear is True. + */ + void loadNotifyEventsFromFile( const QString& filename, bool clear); + + private slots: + void slotFilterMgrFinished(); + void slotFilterMgrStopped(); +}; + +#endif // _SPEECHDATA_H_ diff --git a/kttsd/kttsd/ssmlconvert.cpp b/kttsd/kttsd/ssmlconvert.cpp new file mode 100644 index 0000000..521a9a6 --- /dev/null +++ b/kttsd/kttsd/ssmlconvert.cpp @@ -0,0 +1,295 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + SSMLConvert class + + This class is in charge of converting SSML text into a format that can + be handled by individual synths. + ------------------- + Copyright: + (C) 2004 by Paul Giannaros <ceruleanblaze@gmail.com> + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> +******************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; version 2 of the License. * + * * + ***************************************************************************/ + +// Qt includes. +#include <qstring.h> +#include <qstringlist.h> +#include <qdom.h> +#include <qfile.h> +#include <qtextstream.h> + +// KDE includes. +#include <kdeversion.h> +#include <kstandarddirs.h> +#include <kprocess.h> +#include <ktempfile.h> +#include <kdebug.h> + +// SSMLConvert includes. +#include "ssmlconvert.h" +#include "ssmlconvert.moc" + +/// Constructor. +SSMLConvert::SSMLConvert() { + m_talkers = QStringList(); + m_xsltProc = 0; + m_state = tsIdle; +} + +/// Constructor. Set the talkers to be used as reference for entered text. +SSMLConvert::SSMLConvert(const QStringList &talkers) { + m_talkers = talkers; + m_xsltProc = 0; + m_state = tsIdle; +} + +/// Destructor. +SSMLConvert::~SSMLConvert() { + delete m_xsltProc; + if (!m_inFilename.isEmpty()) QFile::remove(m_inFilename); + if (!m_outFilename.isEmpty()) QFile::remove(m_outFilename); +} + +/// Set the talkers to be used as reference for entered text. +void SSMLConvert::setTalkers(const QStringList &talkers) { + m_talkers = talkers; +} + +QString SSMLConvert::extractTalker(const QString &talkercode) { + QString t = talkercode.section("synthesizer=", 1, 1); + t = t.section('"', 1, 1); + if(t.contains("flite")) + return "flite"; + else + return t.left(t.find(" ")).lower(); +} + +/** +* Return the most appropriate talker for the text to synth talker code. +* @param text the text that will be parsed. +* @returns the appropriate talker for the job as a talker code. +* +* The appropriate talker is the one that has the most features that are required in some +* SSML markup. In the future i'm hoping to make the importance of individual features +* configurable, but better to walk before you can run. +* Currently, the searching method in place is like a filter: Those that meet the criteria we're +* searchin for stay while others are sifted out. This should leave us with the right talker to use. +* It's not a very good method, but should be appropriate in most cases and should do just fine for now. +* +* As it stands, here is the list of things that are looked for, in order of most importance: +* - Language +* Obviously the most important. If a language is specified, look for the talkers that support it. +* Default to en (or some form of en - en_US, en_GB, etc). Only one language at a time is allowed +* at the moment, and must be specified in the root speak element (<speak xml:lang="en-US">) +* - Gender +* If a gender is specified, look for talkers that comply. There is no default so if no gender is +* specified, no talkers will be removed. The only gender that will be searched for is the one +* specified in the root speak element. This should change in the future. +* - Prosody +* Check if prosody modification is allowed by the talker. Currently this is hardcoded (it +* is stated which talkers do and do not in a variable somewhere). +* +* Bear in mind that the XSL stylesheet that will be applied to the SSML is the same regardless +* of the how the talker is chosen, meaning that you don't lose some features of the talker if this +* search doesn't encompass them. +* +* QDom is the item of choice for the matching. Just walk the tree.. +*/ +QString SSMLConvert::appropriateTalker(const QString &text) const { + QDomDocument ssml; + ssml.setContent(text, false); // No namespace processing. + /// Matches are stored here. Obviously to begin with every talker matches. + QStringList matches = m_talkers; + + /// Check that this is (well formed) SSML and all our searching will not be in vain. + QDomElement root = ssml.documentElement(); + if(root.tagName() != "speak") { + // Not SSML. + return QString::null; + } + + /** + * For each rule that we are looking through, iterate over all currently + * matching talkers and remove all the talkers that don't match. + * + * Storage for talker code components. + */ + QString talklang, talkvoice, talkgender, talkvolume, talkrate, talkname; + + kdDebug() << "SSMLConvert::appropriateTalker: BEFORE LANGUAGE SEARCH: " << matches.join(" ") << endl;; + /** + * Language searching + */ + if(root.hasAttribute("xml:lang")) { + QString lang = root.attribute("xml:lang"); + kdDebug() << "SSMLConvert::appropriateTalker: xml:lang found (" << lang << ")" << endl; + /// If it is set to en*, then match all english speakers. They all sound the same anyways. + if(lang.contains("en-")) { + kdDebug() << "SSMLConvert::appropriateTalker: English" << endl; + lang = "en"; + } + /// Find all hits and place them in matches. We don't search for the closing " because if + /// the talker emits lang="en-UK" or something we'll be ignoring it, which we don't what. + matches = matches.grep("lang=\"" + lang); + } + else { + kdDebug() << "SSMLConvert::appropriateTalker: no xml:lang found. Defaulting to en.." << endl; + matches = matches.grep("lang=\"en"); + } + + kdDebug() << "SSMLConvert::appropriateTalker: AFTER LANGUAGE SEARCH: " << matches.join(" ") << endl;; + + /** + * Gender searching + * If, for example, male is specified and only female is found, + * ignore the choice and just use female. + */ + if(root.hasAttribute("gender")) { + QString gender = root.attribute("gender"); + kdDebug() << "SSMLConvert::appropriateTalker: gender found (" << gender << ")" << endl; + /// If the gender found is not 'male' or 'female' then ignore it. + if(!(gender == "male" || gender == "female")) { + /// Make sure that we don't strip away all the talkers because of no matches. + if(matches.grep("gender=\"" + gender).count() >= 1) + matches = matches.grep("gender=\"" + gender); + } + } + else { + kdDebug() << "SSMLConvert::appropriateTalker: no gender found." << endl; + } + + /** + * Prosody + * Search for talkers that allow modification of the synth output - louder, higher, + * slower, etc. There should be a direct way to query each synth to find out if this + * is supported (some function in PlugInConf), but for now, hardcode all the way :( + */ + /// Known to support (feel free to add to the list and if search): + /// Festival Int (not flite), Hadifix + if(matches.grep("synthesizer=\"Festival Interactive").count() >= 1 || + matches.grep("synthesizer=\"Hadifix").count() >= 1) { + + kdDebug() << "SSMLConvert::appropriateTalker: Prosody allowed" << endl; + QStringList tmpmatches = matches.grep("synthesizer=\"Festival Interactive"); + matches = matches.grep("synthesizer=\"Hadifix"); + matches = tmpmatches + matches; + } + else + kdDebug() << "SSMLConvert::appropriateTalker: No prosody-supporting talkers found" << endl; + + /// Return the first match that complies. Maybe a discrete way to + /// choose between all the matches could be offered in the future. Some form of preference. + return matches[0]; +} + +/** +* Applies the spreadsheet for a talker to the SSML and returns the talker-native output. +* @param text The markup to apply the spreadsheet to. +* @param xsltFilename The name of the stylesheet file that will be applied (i.e freetts, flite). +* @returns False if an error occurs. +* +* This converts a piece of SSML into a format the given talker can understand. It applies +* an XSLT spreadsheet to the SSML and returns the output. +* +* Emits transformFinished signal when completed. Caller then calls getOutput to retrieve +* the transformed text. +*/ + +bool SSMLConvert::transform(const QString &text, const QString &xsltFilename) { + m_xsltFilename = xsltFilename; + /// Write @param text to a temporary file. + KTempFile inFile(locateLocal("tmp", "kttsd-"), ".ssml"); + m_inFilename = inFile.file()->name(); + QTextStream* wstream = inFile.textStream(); + if (wstream == 0) { + /// wtf... + kdDebug() << "SSMLConvert::transform: Can't write to " << m_inFilename << endl;; + return false; + } + // TODO: Is encoding an issue here? + // TODO: It would be nice if we detected whether the XML is properly formed + // with the required xml processing instruction and encoding attribute. If + // not wrap it in such. But maybe this should be handled by SpeechData::setText()? + *wstream << text; + inFile.close(); +#if KDE_VERSION >= KDE_MAKE_VERSION (3,3,0) + inFile.sync(); +#endif + + // Get a temporary output file name. + KTempFile outFile(locateLocal("tmp", "kttsd-"), ".output"); + m_outFilename = outFile.file()->name(); + outFile.close(); + // outFile.unlink(); // only activate this if necessary. + + /// Spawn an xsltproc process to apply our stylesheet to our SSML file. + m_xsltProc = new KProcess; + *m_xsltProc << "xsltproc"; + *m_xsltProc << "-o" << m_outFilename << "--novalid" + << m_xsltFilename << m_inFilename; + // Warning: This won't compile under KDE 3.2. See FreeTTS::argsToStringList(). + // kdDebug() << "SSMLConvert::transform: executing command: " << + // m_xsltProc->args() << endl; + + connect(m_xsltProc, SIGNAL(processExited(KProcess*)), + this, SLOT(slotProcessExited(KProcess*))); + if (!m_xsltProc->start(KProcess::NotifyOnExit, KProcess::NoCommunication)) + { + kdDebug() << "SSMLConvert::transform: Error starting xsltproc" << endl; + return false; + } + m_state = tsTransforming; + return true; +} + +void SSMLConvert::slotProcessExited(KProcess* /*proc*/) +{ + m_xsltProc->deleteLater(); + m_xsltProc = 0; + m_state = tsFinished; + emit transformFinished(); +} + +/** +* Returns current processing state. +*/ +int SSMLConvert::getState() { return m_state; } + +/** +* Returns the output from call to transform. +*/ +QString SSMLConvert::getOutput() +{ + /// Read back the data that was written to /tmp/fileName.output. + QFile readfile(m_outFilename); + if(!readfile.open(IO_ReadOnly)) { + /// uhh yeah... Issues writing to the SSML file. + kdDebug() << "SSMLConvert::slotProcessExited: Could not read file " << m_outFilename << endl; + return QString::null; + } + QTextStream rstream(&readfile); + QString convertedData = rstream.read(); + readfile.close(); + + // kdDebug() << "SSMLConvert::slotProcessExited: Read SSML file at " + m_inFilename + " and created " + m_outFilename + " based on the stylesheet at " << m_xsltFilename << endl; + + // Clean up. + QFile::remove(m_inFilename); + m_inFilename = QString::null; + QFile::remove(m_outFilename); + m_outFilename = QString::null; + + // Ready for another transform. + m_state = tsIdle; + + return convertedData; +} + diff --git a/kttsd/kttsd/ssmlconvert.h b/kttsd/kttsd/ssmlconvert.h new file mode 100644 index 0000000..1ee332c --- /dev/null +++ b/kttsd/kttsd/ssmlconvert.h @@ -0,0 +1,129 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + SSMLConvert class + + This class is in charge of converting SSML text into a format that can + be handled by individual synths. + ------------------- + Copyright: + (C) 2004 by Paul Giannaros <ceruleanblaze@gmail.com> + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Paul Giannaros <ceruleanblaze@gmail.com> +******************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; version 2 of the License. * + * * + ***************************************************************************/ + +#ifndef _SSMLCONVERT_H_ +#define _SSMLCONVERT_H_ + +/** + * SsmlConvert class: + * Receives a QStringList of talkers and, based on that information, + * evaluates received SSML to discover which of the given talkers best + * suits it. It can then convert the given SSML into a format understandable + * by the talker. + */ + +// Qt includes +#include <qobject.h> +#include <qstringlist.h> + +class KProcess; +class QString; + +class SSMLConvert : public QObject { + Q_OBJECT +public: + /** Constructors */ + SSMLConvert(); + SSMLConvert(const QStringList &talkers); + /** Destructor */ + virtual ~SSMLConvert(); + + enum TransformState { + tsIdle = 0, // Not doing anything. Ready to transform. + tsTransforming = 1, // Transforming. + tsFinished = 2 // Transforming finished. + }; + + /** + * Set the talker codes to be used. + * @param talkers talker codes to be used. + */ + void setTalkers(const QStringList &talkers); + + /** + * Extract the synth name from a talker code (i.e festival, flite, freetts). + * @param talkercode the talker code to extract the talker from. + * @returns the talker. + */ + QString extractTalker(const QString &talkercode); + + /** + * Returns the most appropriate talker for the text to synth's talker code. + * @param text the text that will be parsed. + * @returns the appropriate talker for the job as a talker code QString. + * + * The appropriate talker is the one that has the most features that are required in some + * SSML markup. In the future i'm hoping to make the importance of individual features + * configurable, but better to walk before you can run. + * Currently, the searching method in place is like a filter: Those that meet the criteria we're + * searchin for stay while others are sifted out. This should leave us with the right talker to use. + * It's not a very good method, but should be appropriate in most cases and should do just fine for now. + * + * See the implementation file for more detail. + */ + QString appropriateTalker(const QString &text) const; + + /** + * Applies the spreadsheet for a talker to the SSML and returns the talker-native output. + * @param text the markup to apply the spreadsheet to. + * @param xsltFilename the name of the stylesheet file that will be applied (i.e freetts, flite). + * @returns the output that the synth can understand. + * + * This converts a piece of SSML into a format the given talker can understand. It applies + * an XSLT spreadsheet to the SSML and returns the output. + */ + bool transform(const QString &text, const QString &xsltFilename); + + /** + * Returns current processing state. + */ + int getState(); + + /** + * Returns the output from call to transform. + */ + QString getOutput(); + +signals: + /** + * Emitted whenever tranforming is completed. + */ + void transformFinished(); + +private slots: + void slotProcessExited(KProcess* proc); + +private: + /// The XSLT processor. + KProcess *m_xsltProc; + /// Current talkers. + QStringList m_talkers; + // Current state. + int m_state; + // Name of XSLT file. + QString m_xsltFilename; + // Name of temporary input file. + QString m_inFilename; + // Name of temporary output file. + QString m_outFilename; +}; + +#endif // _SSMLCONVERT_H_ diff --git a/kttsd/kttsd/talkermgr.cpp b/kttsd/kttsd/talkermgr.cpp new file mode 100644 index 0000000..345f12b --- /dev/null +++ b/kttsd/kttsd/talkermgr.cpp @@ -0,0 +1,388 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Manages all the Talker (synth) plugins. + ------------------- + Copyright: + (C) 2004-2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Gary Cramblitt <garycramblitt@comcast.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +// Qt includes. + +// KDE includes. +#include <kdebug.h> +#include <kparts/componentfactory.h> +#include <ktrader.h> +#include <kstandarddirs.h> + +// KTTS includes. +#include "pluginconf.h" +#include "talkermgr.h" +#include "threadedplugin.h" + +/** + * Constructor. + */ +TalkerMgr::TalkerMgr(QObject *parent, const char *name) : + QObject( parent, name ) +{ + m_loadedPlugIns.setAutoDelete(true); +} + +/** + * Destructor. + */ +TalkerMgr::~TalkerMgr() +{ + m_loadedPlugIns.clear(); +} + +/** + * Load all the configured synth plugins, populating loadedPlugIns structure. + */ +int TalkerMgr::loadPlugIns(KConfig* config) +{ + // kdDebug() << "Running: TalkerMgr::loadPlugIns()" << endl; + int good = 0; + int bad = 0; + + m_talkerToPlugInCache.clear(); + m_loadedPlugIns.clear(); + m_loadedTalkerCodes.clear(); + m_loadedTalkerIds.clear(); + + config->setGroup("General"); + QStringList talkerIDsList = config->readListEntry("TalkerIDs", ','); + if (!talkerIDsList.isEmpty()) + { + KLibFactory *factory; + QStringList::ConstIterator itEnd(talkerIDsList.constEnd()); + for( QStringList::ConstIterator it = talkerIDsList.constBegin(); it != itEnd; ++it ) + { + // kdDebug() << "Loading plugInProc for Talker ID " << *it << endl; + + // Talker ID. + QString talkerID = *it; + + // Set the group for the language we're loading + config->setGroup("Talker_" + talkerID); + + // Get the DesktopEntryName of the plugin we will try to load. + QString desktopEntryName = config->readEntry("DesktopEntryName", QString::null); + + // If a DesktopEntryName is not in the config file, it was configured before + // we started using them, when we stored translated plugin names instead. + // Try to convert the translated plugin name to a DesktopEntryName. + // DesktopEntryNames are better because user can change their desktop language + // and DesktopEntryName won't change. + if (desktopEntryName.isEmpty()) + { + QString synthName = config->readEntry("PlugIn", QString::null); + // See if the translated name will untranslate. If not, well, sorry. + desktopEntryName = TalkerCode::TalkerNameToDesktopEntryName(synthName); + // Record the DesktopEntryName from now on. + if (!desktopEntryName.isEmpty()) config->writeEntry("DesktopEntryName", desktopEntryName); + } + + // Get the talker code. + QString talkerCode = config->readEntry("TalkerCode", QString::null); + + // Normalize the talker code. + QString fullLanguageCode; + talkerCode = TalkerCode::normalizeTalkerCode(talkerCode, fullLanguageCode); + + // Find the KTTSD SynthPlugin. + KTrader::OfferList offers = KTrader::self()->query( + "KTTSD/SynthPlugin", QString("DesktopEntryName == '%1'").arg(desktopEntryName)); + + if(offers.count() > 1){ + ++bad; + kdDebug() << "More than 1 plug in doesn't make any sense, well, let's use any" << endl; + } else if(offers.count() < 1){ + ++bad; + kdDebug() << "Less than 1 plug in, nothing can be done" << endl; + } else { + kdDebug() << "Loading " << offers[0]->library() << endl; + factory = KLibLoader::self()->factory(offers[0]->library().latin1()); + if(factory){ + PlugInProc *speech = + KParts::ComponentFactory::createInstanceFromLibrary<PlugInProc>( + offers[0]->library().latin1(), this, offers[0]->library().latin1()); + if(!speech){ + kdDebug() << "Couldn't create the speech object from " << offers[0]->library() << endl; + ++bad; + } else { + if (speech->supportsAsync()) + { + speech->init(config, "Talker_" + talkerID); + // kdDebug() << "Plug in " << desktopEntryName << " created successfully." << endl; + m_loadedPlugIns.append(speech); + } else { + // Synchronous plugins are run in a separate thread. + // Init will start the thread and it will immediately go to sleep. + QString threadedPlugInName = QString::fromLatin1("threaded") + desktopEntryName; + ThreadedPlugIn* speechThread = new ThreadedPlugIn(speech, + this, threadedPlugInName.latin1()); + speechThread->init(config, "Talker_" + talkerCode); + // kdDebug() << "Threaded Plug in " << desktopEntryName << " for language " << (*it).right((*it).length()-5) << " created succesfully." << endl; + m_loadedPlugIns.append(speechThread); + } + ++good; + m_loadedTalkerCodes.append(TalkerCode(talkerCode)); + m_loadedTalkerIds.append(talkerID); + } + } else { + kdDebug() << "Couldn't create the factory object from " << offers[0]->library() << endl; + ++bad; + } + } + } + } + if(bad > 0){ + if(good == 0){ + // No plugin could be loaded. + return -1; + } else { + // At least one plugin was loaded and one failed. + return 0; + } + } else { + if (good == 0) + // No plugin could be loaded. + return -1; + else + // All the plug in were loaded perfectly + return 1; + } +} + +/** + * Get a list of the talkers configured in KTTS. + * @return A QStringList of fully-specified talker codes, one + * for each talker user has configured. + */ +QStringList TalkerMgr::getTalkers() +{ + QStringList talkerList; + for (int ndx = 0; ndx < int(m_loadedPlugIns.count()); ++ndx) + { + talkerList.append(m_loadedTalkerCodes[ndx].getTalkerCode()); + } + return talkerList; +} + +/** + * Returns a list of all the loaded plugins. + */ +QPtrList<PlugInProc> TalkerMgr::getLoadedPlugIns() +{ + return m_loadedPlugIns; +} + +/** + * Given a talker code, returns pointer to the closest matching plugin. + * @param talker The talker (language) code. + * @return Index to m_loadedPlugins array of Talkers. + * + * If a plugin has not been loaded to match the talker, returns the default + * plugin. + */ +int TalkerMgr::talkerToPluginIndex(const QString& talker) const +{ + // kdDebug() << "TalkerMgr::talkerToPluginIndex: matching talker " << talker << " to closest matching plugin." << endl; + // If we have a cached match, return that. + if (m_talkerToPlugInCache.contains(talker)) + return m_talkerToPlugInCache[talker]; + else + { + int winner = TalkerCode::findClosestMatchingTalker(m_loadedTalkerCodes, talker, true); + m_talkerToPlugInCache[talker] = winner; + return winner; + } +} + +/** + * Given a talker code, returns pointer to the closest matching plugin. + * @param talker The talker (language) code. + * @return Pointer to closest matching plugin. + * + * If a plugin has not been loaded to match the talker, returns the default + * plugin. + * + * TODO: When picking a talker, %KTTSD will automatically determine if text contains + * markup and pick a talker that supports that markup, if available. This + * overrides all other attributes, i.e, it is treated as an automatic "top priority" + * attribute. + */ +PlugInProc* TalkerMgr::talkerToPlugin(const QString& talker) const +{ + int talkerNdx = talkerToPluginIndex(talker); + return const_cast< QPtrList<PlugInProc>* >(&m_loadedPlugIns)->at(talkerNdx); +} + +/** + * Given a talker code, returns the parsed TalkerCode of the closest matching Talker. + * @param talker The talker (language) code. + * @return Parsed TalkerCode structure. + * + * If a plugin has not been loaded to match the talker, returns the default + * plugin. + * + * The returned TalkerCode is a copy and should be destroyed by caller. + * + * TODO: When picking a talker, %KTTSD will automatically determine if text contains + * markup and pick a talker that supports that markup, if available. This + * overrides all other attributes, i.e, it is treated as an automatic "top priority" + * attribute. + */ +TalkerCode* TalkerMgr::talkerToTalkerCode(const QString& talker) +{ + int talkerNdx = talkerToPluginIndex(talker); + return new TalkerCode(&m_loadedTalkerCodes[talkerNdx]); +} + +/** + * Given a Talker Code, returns the Talker ID of the talker that would speak + * a text job with that Talker Code. + * @param talkerCode Talker Code. + * @return Talker ID of the talker that would speak the text job. + */ +QString TalkerMgr::talkerCodeToTalkerId(const QString& talkerCode) +{ + int talkerNdx = talkerToPluginIndex(talkerCode); + return m_loadedTalkerIds[talkerNdx]; +} + +/** + * Get the user's default talker. + * @return A fully-specified talker code. + * + * @see talkers + * @see getTalkers + */ +QString TalkerMgr::userDefaultTalker() const +{ + return m_loadedTalkerCodes[0].getTalkerCode(); +} + +/** + * Determine whether the currently-configured speech plugin supports a speech markup language. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * @param markupType The kttsd code for the desired speech markup language. + * @return True if the plugin currently configured for the indicated + * talker supports the indicated speech markup language. + * @see kttsdMarkupType + */ +bool TalkerMgr::supportsMarkup(const QString& talker, const uint /*markupType*/) const +{ + kdDebug() << "TalkerMgr::supportsMarkup: Testing talker " << talker << endl; + QString matchingTalker = talker; + if (matchingTalker.isEmpty()) matchingTalker = userDefaultTalker(); + PlugInProc* plugin = talkerToPlugin(matchingTalker); + return ( plugin->getSsmlXsltFilename() != + KGlobal::dirs()->resourceDirs("data").last() + "kttsd/xslt/SSMLtoPlainText.xsl"); +} + +bool TalkerMgr::autoconfigureTalker(const QString& langCode, KConfig* config) +{ + // Not yet implemented. + // return false; + + QString languageCode = langCode; + + // Get last TalkerID from config. + QStringList talkerIDsList = config->readListEntry("TalkerIDs", ','); + int lastTalkerID = 0; + for (uint talkerIdNdx = 0; talkerIdNdx < talkerIDsList.count(); ++talkerIdNdx) + { + int id = talkerIDsList[talkerIdNdx].toInt(); + if (id > lastTalkerID) lastTalkerID = id; + } + + // Assign a new Talker ID for the talker. Wraps around to 1. + QString talkerID = QString::number(lastTalkerID + 1); + + // Query for all the KTTSD SynthPlugins. + KTrader::OfferList offers = KTrader::self()->query("KTTSD/SynthPlugin"); + + // Iterate thru the possible plug ins. + for(unsigned int i=0; i < offers.count() ; ++i) + { + // See if this plugin supports the desired language. + QStringList languageCodes = offers[i]->property("X-KDE-Languages").toStringList(); + if (languageCodes.contains(languageCode)) + { + QString desktopEntryName = offers[i]->desktopEntryName(); + + // Load the plugin. + KLibFactory *factory = KLibLoader::self()->factory(offers[0]->library().latin1()); + if (factory) + { + // If the factory is created successfully, instantiate the PlugInConf class for the + // specific plug in to get the plug in configuration object. + PlugInConf* loadedTalkerPlugIn = + KParts::ComponentFactory::createInstanceFromLibrary<PlugInConf>( + offers[0]->library().latin1(), NULL, offers[0]->library().latin1()); + if (loadedTalkerPlugIn) + { + // Give plugin the language code and permit plugin to autoconfigure itself. + loadedTalkerPlugIn->setDesiredLanguage(languageCode); + loadedTalkerPlugIn->load(config, QString("Talker_")+talkerID); + + // If plugin was able to configure itself, it returns a full talker code. + QString talkerCode = loadedTalkerPlugIn->getTalkerCode(); + + if (!talkerCode.isEmpty()) + { + // Erase extraneous Talker configuration entries that might be there. + config->deleteGroup(QString("Talker_")+talkerID); + + // Let plugin save its configuration. + config->setGroup(QString("Talker_")+talkerID); + loadedTalkerPlugIn->save(config, QString("Talker_"+talkerID)); + + // Record configuration data. + config->setGroup(QString("Talker_")+talkerID); + config->writeEntry("DesktopEntryName", desktopEntryName); + talkerCode = TalkerCode::normalizeTalkerCode(talkerCode, languageCode); + config->writeEntry("TalkerCode", talkerCode); + + // Add TalkerID to configured list. + talkerIDsList.append(talkerID); + config->setGroup("General"); + config->writeEntry("TalkerIDs", talkerIDsList.join(",")); + config->sync(); + + // TODO: Now that we have modified the config, need a way to inform + // other apps, including KTTSMgr. As this routine is likely called + // when KTTSMgr is not running, is not a serious problem. + + // Success! + delete loadedTalkerPlugIn; + return true; + } + + // Plugin no longer needed. + delete loadedTalkerPlugIn; + } + } + } + } + + return false; +} diff --git a/kttsd/kttsd/talkermgr.h b/kttsd/kttsd/talkermgr.h new file mode 100644 index 0000000..5b890ae --- /dev/null +++ b/kttsd/kttsd/talkermgr.h @@ -0,0 +1,159 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Manages all the Talker (synth) plugins. + ------------------- + Copyright: + (C) 2004-2005 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Gary Cramblitt <garycramblitt@comcast.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _TALKERMGR_H_ +#define _TALKERMGR_H_ + +// Qt includes. +#include <qstring.h> +#include <qstringlist.h> +#include <qmap.h> +#include <qptrlist.h> + +// KTTS includes. +#include "talkercode.h" +#include "pluginproc.h" + +class TalkerMgr: public QObject +{ +public: + + /** + * Constructor. + */ + TalkerMgr(QObject *parent = 0, const char *name = 0); + + /** + * Destructor. + */ + ~TalkerMgr(); + + /** + * Load all the configured plug ins populating loadedPlugIns + */ + int loadPlugIns(KConfig* config); + + /** + * Get a list of the talkers configured in KTTS. + * @return A QStringList of fully-specified talker codes, one + * for each talker user has configured. + */ + QStringList getTalkers(); + + /** + * Returns a list of all the loaded plugins. + */ + QPtrList<PlugInProc> getLoadedPlugIns(); + + /** + * Given a talker code, returns pointer to the closest matching plugin. + * @param talker The talker (language) code. + * @return Index to m_loadedPlugins array of Talkers. + * + * If a plugin has not been loaded to match the talker, returns the default + * plugin. + */ + int talkerToPluginIndex(const QString& talker) const; + + /** + * Given a talker code, returns pointer to the closest matching plugin. + * @param talker The talker (language) code. + * @return Pointer to closest matching plugin. + * + * If a plugin has not been loaded to match the talker, returns the default + * plugin. + */ + PlugInProc* talkerToPlugin(const QString& talker) const; + + /** + * Given a talker code, returns the parsed TalkerCode of the closest matching Talker. + * @param talker The talker (language) code. + * @return Parsed TalkerCode structure. + * + * If a plugin has not been loaded to match the talker, returns the default + * plugin. + * + * The returned TalkerCode is a copy and should be destroyed by caller. + * + * TODO: When picking a talker, %KTTSD will automatically determine if text contains + * markup and pick a talker that supports that markup, if available. This + * overrides all other attributes, i.e, it is treated as an automatic "top priority" + * attribute. + */ + TalkerCode* talkerToTalkerCode(const QString& talker); + + /** + * Given a Talker Code, returns the Talker ID of the talker that would speak + * a text job with that Talker Code. + * @param talkerCode Talker Code. + * @return Talker ID of the talker that would speak the text job. + */ + QString talkerCodeToTalkerId(const QString& talkerCode); + + /** + * Get the user's default talker. + * @return A fully-specified talker code. + * + * @see talkers + * @see getTalkers + */ + QString userDefaultTalker() const; + + /** + * Determine whether the currently-configured speech plugin supports a speech markup language. + * @param talker Code for the talker to do the speaking. Example "en". + * If NULL, defaults to the user's default talker. + * @param markupType The kttsd code for the desired speech markup language. + * @return True if the plugin currently configured for the indicated + * talker supports the indicated speech markup language. + * @see kttsdMarkupType + */ + bool supportsMarkup(const QString& talker, const uint markupType) const; + + /** + * Try to automatically configure a Talker in the specified language. + * @param langCode Two-letter language code. + * @param config KConfig to be updated if successful. + * @return True if successful. + * + * If successful, the KConfig rc file is updated but the talker has not been loaded. + */ + bool autoconfigureTalker(const QString& langCode, KConfig* config); + +private: + + /** + * Array of the loaded plug ins for different Talkers. + * Array of parsed Talker Codes for the plugins. + */ + QPtrList<PlugInProc> m_loadedPlugIns; + QStringList m_loadedTalkerIds; + TalkerCode::TalkerCodeList m_loadedTalkerCodes; + + /** + * Cache of talker codes and index of closest matching Talker. + */ + mutable QMap<QString,int> m_talkerToPlugInCache; +}; + +#endif // _TALKERMGR_H_ diff --git a/kttsd/kttsd/threadedplugin.cpp b/kttsd/kttsd/threadedplugin.cpp new file mode 100644 index 0000000..babc792 --- /dev/null +++ b/kttsd/kttsd/threadedplugin.cpp @@ -0,0 +1,282 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Converts a synchronous plugin into an asynchronous one. + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Gary Cramblitt <garycramblitt@comcast.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#include <qevent.h> +#include <qapplication.h> + +#include <kdebug.h> + +#include "speaker.h" +#include "threadedplugin.h" + +/** +* Constructor. +*/ +ThreadedPlugIn::ThreadedPlugIn(PlugInProc* plugin, + QObject *parent /*= 0*/, const char *name /*= 0*/): + PlugInProc(parent, name), + QThread(), + m_plugin(plugin), + m_filename(QString::null), + m_requestExit(false), + m_supportsSynth(false) +{ + m_waitingStop = false; + m_state = psIdle; +} + +/** +* Destructor. +*/ +ThreadedPlugIn::~ThreadedPlugIn() +{ + if (running()) + { + // If thread is busy, try to stop it. + m_requestExit = true; + if (m_threadRunningMutex.locked()) m_plugin->stopText(); + m_action = paNone; + m_waitCondition.wakeOne(); + wait(5000); + // If thread still active, stopText didn't succeed. Terminate the thread. + if (running()) + { + terminate(); + wait(); + } + } + delete m_plugin; +} + +/** +* Initialize the speech plugin. +*/ +bool ThreadedPlugIn::init(KConfig *config, const QString &configGroup) +{ + bool stat = m_plugin->init(config, configGroup); + m_supportsSynth = m_plugin->supportsSynth(); + // Start thread running, which will immediately go to sleep. + start(); + return stat; +} + +/** +* Say a text. Synthesize and audibilize it. +* @param text The text to be spoken. +* +* If the plugin supports asynchronous operation, it should return immediately +* and emit sayFinished signal when synthesis and audibilizing is finished. +*/ +void ThreadedPlugIn::sayText(const QString &text) +{ + kdDebug() << "ThreadedPlugin::sayText running with text " << text << endl; + waitThreadNotBusy(); + m_action = paSayText; + m_text = text; + m_state = psSaying; + m_waitCondition.wakeOne(); +} + +/** +* Synthesize text into an audio file, but do not send to the audio device. +* @param text The text to be synthesized. +* @param suggestedFilename Full pathname of file to create. The plugin +* may ignore this parameter and choose its own +* filename. KTTSD will query the generated +* filename using getFilename(). +* +* If the plugin supports asynchronous operation, it should return immediately +* and emit synthFinished signal when synthesis is completed. +*/ +void ThreadedPlugIn::synthText(const QString &text, const QString &suggestedFilename) +{ + waitThreadNotBusy(); + m_action = paSynthText; + m_text = text; + m_filename = suggestedFilename; + m_state = psSynthing; + m_waitCondition.wakeOne(); +} + +/** +* Get the generated audio filename from synthText. +* @return Name of the audio file the plugin generated. +* Null if no such file. +* +* The plugin must not re-use the filename. +*/ +QString ThreadedPlugIn::getFilename() +{ + return m_filename; +} + +/** +* Stop current operation (saying or synthesizing text). +* This function only makes sense in asynchronus modes. +* The plugin should return to the psIdle state. +*/ +void ThreadedPlugIn::stopText() +{ + // If thread is busy, call stopText and wait for thread to stop being busy. + if (m_threadRunningMutex.locked()) + { + kdDebug() << "ThreadedPlugIn::stopText:: calling m_plugin->stopText" << endl; + m_plugin->stopText(); + // Set flag that will force state to idle once the plugin finishes. + m_waitingStop = true; +// waitThreadNotBusy(); + } else m_state = psIdle; +} + +/** +* Return the current state of the plugin. +* This function only makes sense in asynchronous mode. +* @return The pluginState of the plugin. +* +* @ref pluginState +*/ +pluginState ThreadedPlugIn::getState() +{ + m_stateMutex.unlock(); + bool emitStopped = false; + // If stopText was called, plugin may not have truly stopped, in which + // case, if has finally completed the operation, return idle state. + if (m_waitingStop) + { + if (m_state == psFinished) m_state = psIdle; + if (m_state == psIdle) + { + m_waitingStop = false; + emitStopped = true; + } + } + pluginState plugState = m_state; + m_stateMutex.unlock(); + if (emitStopped) emit stopped(); + return plugState; +} + +/** +* Acknowleges a finished state and resets the plugin state to psIdle. +* +* If the plugin is not in state psFinished, nothing happens. +* The plugin may use this call to do any post-processing cleanup, +* for example, blanking the stored filename (but do not delete the file). +* Calling program should call getFilename prior to ackFinished. +*/ +void ThreadedPlugIn::ackFinished() +{ + // Since plugin should not be running, don't bother with Mutex here. + if (m_state == psFinished) m_state = psIdle; + m_filename = QString::null; +} + +/** +* Returns True if the plugin supports asynchronous processing, +* i.e., returns immediately from sayText or synthText. +* @return True if this plugin supports asynchronous processing. +* +* Since this is a threaded wrapper, return True. +*/ +bool ThreadedPlugIn::supportsAsync() { return true; } + +/** +* Returns True if the plugin supports synthText method, +* i.e., is able to synthesize text to a sound file without +* audibilizing the text. +* @return True if this plugin supports synthText method. +*/ +bool ThreadedPlugIn::supportsSynth() { return m_supportsSynth; } + +/** +* Waits for the thread to go to sleep. +*/ +void ThreadedPlugIn::waitThreadNotBusy() +{ + m_threadRunningMutex.lock(); + m_threadRunningMutex.unlock(); +} + +/** +* Base function, where the thread will start. +*/ +void ThreadedPlugIn::run() +{ + while (!m_requestExit) + { + + if (!m_threadRunningMutex.locked()) m_threadRunningMutex.lock(); + // Go to sleep until asked to do something. + // Mutex unlocks as we go to sleep and locks as we wake up. + kdDebug() << "ThreadedPlugIn::run going to sleep." << endl; + m_waitCondition.wait(&m_threadRunningMutex); + kdDebug() << "ThreadedPlugIn::run waking up." << endl; + // Woken up. + // See if we've been told to exit. + if (m_requestExit) + { + m_threadRunningMutex.unlock(); + return; + } + + // Branch on requested action. + switch( m_action ) + { + case paNone: break; + + case paSayText: + { + m_stateMutex.lock(); + m_state = psSaying; + m_stateMutex.unlock(); + kdDebug() << "ThreadedPlugIn::run calling sayText" << endl; + m_plugin->sayText(m_text); + kdDebug() << "ThreadedPlugIn::run back from sayText" << endl; + m_stateMutex.lock(); + if (m_state == psSaying) m_state = psFinished; + m_stateMutex.unlock(); + emit sayFinished(); + break; + } + + case paSynthText: + { + m_stateMutex.lock(); + m_state = psSynthing; + m_stateMutex.unlock(); + QString filename = m_filename; + m_filename = QString::null; + kdDebug() << "ThreadedPlugIn::run calling synthText" << endl; + m_plugin->synthText(m_text, filename); + kdDebug() << "ThreadedPlugIn::run back from synthText" << endl; + m_filename = m_plugin->getFilename(); + m_stateMutex.lock(); + if (m_state == psSynthing) m_state = psFinished; + m_stateMutex.unlock(); + emit synthFinished(); + break; + } + } + } + if (m_threadRunningMutex.locked()) m_threadRunningMutex.unlock(); +} diff --git a/kttsd/kttsd/threadedplugin.h b/kttsd/kttsd/threadedplugin.h new file mode 100644 index 0000000..bc9ed6a --- /dev/null +++ b/kttsd/kttsd/threadedplugin.h @@ -0,0 +1,200 @@ +/***************************************************** vim:set ts=4 sw=4 sts=4: + Converts a synchronous plugin into an asynchronous one. + ------------------- + Copyright: + (C) 2004 by Gary Cramblitt <garycramblitt@comcast.net> + ------------------- + Original author: Gary Cramblitt <garycramblitt@comcast.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ******************************************************************************/ + +#ifndef _THREADEDPLUGIN_H_ +#define _THREADEDPLUGIN_H_ + +#include <qthread.h> +#include <qmutex.h> +#include <qwaitcondition.h> + +#include "pluginproc.h" + +class Speaker; + +class ThreadedPlugIn : public PlugInProc, public QThread +{ + public: + enum pluginAction + { + paNone = 0, + paSayText = 1, + paSynthText = 2 + }; + + /** + * Constructor. + */ + ThreadedPlugIn(PlugInProc* plugin, QObject *parent = 0, const char *name = 0); + + /** + * Destructor. + */ + virtual ~ThreadedPlugIn(); + + /** + * Initializate the speech plugin. + */ + virtual bool init(KConfig *config, const QString &configGroup); + + /** + * Say a text. Synthesize and audibilize it. + * @param text The text to be spoken. + * + * If the plugin supports asynchronous operation, it should return immediately + * and emit sayFinished signal when synthesis and audibilizing is finished. + */ + virtual void sayText(const QString &text); + + /** + * Synthesize text into an audio file, but do not send to the audio device. + * @param text The text to be synthesized. + * @param suggestedFilename Full pathname of file to create. The plugin + * may ignore this parameter and choose its own + * filename. KTTSD will query the generated + * filename using getFilename(). + * + * If the plugin supports asynchronous operation, it should return immediately + * and emit synthFinished signal when synthesis is completed. + */ + virtual void synthText(const QString &text, const QString &suggestedFilename); + + /** + * Get the generated audio filename from synthText. + * @return Name of the audio file the plugin generated. + * Null if no such file. + * + * The plugin must not re-use the filename. + */ + virtual QString getFilename(); + + /** + * Stop current operation (saying or synthesizing text). + * This function only makes sense in asynchronus modes. + * The plugin should return to the psIdle state. + */ + virtual void stopText(); + + /** + * Return the current state of the plugin. + * This function only makes sense in asynchronous mode. + * @return The pluginState of the plugin. + * + * @ref pluginState + */ + virtual pluginState getState(); + + /** + * Acknowleges a finished state and resets the plugin state to psIdle. + * + * If the plugin is not in state psFinished, nothing happens. + * The plugin may use this call to do any post-processing cleanup, + * for example, blanking the stored filename (but do not delete the file). + * Calling program should call getFilename prior to ackFinished. + */ + virtual void ackFinished(); + + /** + * Returns True if the plugin supports asynchronous processing, + * i.e., returns immediately from sayText or synthText. + * @return True if this plugin supports asynchronous processing. + */ + virtual bool supportsAsync(); + + /** + * Returns True if the plugin supports synthText method, + * i.e., is able to synthesize text to a sound file without + * audibilizing the text. + * @return True if this plugin supports synthText method. + */ + virtual bool supportsSynth(); + + protected: + /** + * Base function, where the thread will start. + */ + virtual void run(); + + private: + /** + * Waits for the thread to go to sleep. + */ + void waitThreadNotBusy(); + + /** + * The plugin we wrap. + */ + PlugInProc* m_plugin; + + /** + * An action requested of the plugin. + */ + pluginAction m_action; + + /** + * A text buffer to go with an action (if applicable). + */ + QString m_text; + + /** + * Current state of the plugin. + */ + volatile pluginState m_state; + + /** + * Mutext for accessing state variable. + */ + QMutex m_stateMutex; + + /** + * True when stopText was called but the plugin did not stop. + */ + bool m_waitingStop; + + /** + * Locked when thread is running. + */ + QMutex m_threadRunningMutex; + + /** + * Filename for generated synthesized text. + */ + QString m_filename; + + /** + * Thread wait condition. + */ + QWaitCondition m_waitCondition; + + /** + * Thread exit flag. + */ + volatile bool m_requestExit; + + /** + * Whether wrapped plugin supports separate synthesis. + */ + bool m_supportsSynth; +}; + +#endif // _THREADEDPLUGIN_H_ |