summaryrefslogtreecommitdiffstats
path: root/kttsd/kttsd/speechdata.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'kttsd/kttsd/speechdata.cpp')
-rw-r--r--kttsd/kttsd/speechdata.cpp1275
1 files changed, 1275 insertions, 0 deletions
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();
+}
+