diff options
author | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
---|---|---|
committer | toma <toma@283d02a7-25f6-0310-bc7c-ecb5cbfe19da> | 2009-11-25 17:56:58 +0000 |
commit | 00bb99ac80741fc50ef8a289719373032f2391eb (patch) | |
tree | 3a5a9bf72f942784b38bf77dd66c534662fab5f2 /kttsd/kttsd/speechdata.cpp | |
download | tdeaccessibility-00bb99ac80741fc50ef8a289719373032f2391eb.tar.gz tdeaccessibility-00bb99ac80741fc50ef8a289719373032f2391eb.zip |
Copy the KDE 3.5 branch to branches/trinity for new KDE 3.5 features.
BUG:215923
git-svn-id: svn://anonsvn.kde.org/home/kde/branches/trinity/kdeaccessibility@1054174 283d02a7-25f6-0310-bc7c-ecb5cbfe19da
Diffstat (limited to 'kttsd/kttsd/speechdata.cpp')
-rw-r--r-- | kttsd/kttsd/speechdata.cpp | 1275 |
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(); +} + |