summaryrefslogtreecommitdiffstats
path: root/kttsd/kttsd
diff options
context:
space:
mode:
Diffstat (limited to 'kttsd/kttsd')
-rw-r--r--kttsd/kttsd/Makefile.am51
-rw-r--r--kttsd/kttsd/SSMLtoPlainText.xsl9
-rw-r--r--kttsd/kttsd/filtermgr.cpp405
-rw-r--r--kttsd/kttsd/filtermgr.h199
-rw-r--r--kttsd/kttsd/kttsd.cpp1183
-rw-r--r--kttsd/kttsd/kttsd.desktop56
-rw-r--r--kttsd/kttsd/kttsd.h686
-rw-r--r--kttsd/kttsd/main.cpp68
-rw-r--r--kttsd/kttsd/speaker.cpp1701
-rw-r--r--kttsd/kttsd/speaker.h599
-rw-r--r--kttsd/kttsd/speechdata.cpp1275
-rw-r--r--kttsd/kttsd/speechdata.h731
-rw-r--r--kttsd/kttsd/ssmlconvert.cpp295
-rw-r--r--kttsd/kttsd/ssmlconvert.h129
-rw-r--r--kttsd/kttsd/talkermgr.cpp388
-rw-r--r--kttsd/kttsd/talkermgr.h159
-rw-r--r--kttsd/kttsd/threadedplugin.cpp282
-rw-r--r--kttsd/kttsd/threadedplugin.h200
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_