You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdeaccessibility/kttsd/kttsd/speechdata.cpp

1276 lines
45 KiB

/***************************************************** 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>
// TQt includes.
#include <tqregexp.h>
#include <tqpair.h>
#include <tqvaluelist.h>
#include <tqdom.h>
#include <tqfile.h>
// KDE includes.
#include <kdebug.h>
#include <tdeglobal.h>
#include <kstandarddirs.h>
#include <tdeapplication.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 = TDEGlobal::config();
config = new TDEConfig("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.
TQPtrListIterator<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, TQT_SIGNAL(filteringFinished()), this, TQT_SLOT(slotFilterMgrFinished()));
connect (filterMgr, TQT_SIGNAL(filteringStopped()), this, TQT_SLOT(slotFilterMgrStopped()));
m_pooledFilterMgrs.append(pooledFilterMgr);
return true;
}
/**
* Loads notify events from a file. Clearing data if clear is True.
*/
void SpeechData::loadNotifyEventsFromFile( const TQString& filename, bool clear)
{
// Open existing event list.
TQFile file( filename );
if ( !file.open( IO_ReadOnly ) )
{
kdDebug() << "SpeechData::loadNotifyEventsFromFile: Unable to open file " << filename << endl;
}
// TQDomDocument doc( "http://www.kde.org/share/apps/kttsd/stringreplacer/wordlist.dtd []" );
TQDomDocument 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 = TQString();
notifyDefaultOptions.customMsg = TQString();
notifyAppMap.clear();
}
// Event list.
TQDomNodeList eventList = doc.elementsByTagName("notifyEvent");
const int eventListCount = eventList.count();
for (int eventIndex = 0; eventIndex < eventListCount; ++eventIndex)
{
TQDomNode eventNode = eventList.item(eventIndex);
TQDomNodeList propList = eventNode.childNodes();
TQString eventSrc;
TQString event;
TQString actionName;
TQString message;
TalkerCode talkerCode;
const int propListCount = propList.count();
for (int propIndex = 0; propIndex < propListCount; ++propIndex)
{
TQDomNode propNode = propList.item(propIndex);
TQDomElement 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 = TQString();
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);
}
TQPtrListIterator<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 TQString &msg, const TQString &talker, const TQCString &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 TQString &warning, const TQString &talker, const TQCString &appId){
// kdDebug() << "Running: SpeechData::enqueueWarning( const TQString &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 = TQStringList();
// 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 TQString &message, const TQString &talker, const TQCString& 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 = TQStringList();
// 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 TQString &text)
{
/// This checks to see if the root tag of the text is a <speak> tag.
TQDomDocument ssml;
ssml.setContent(text, false); // No namespace processing.
/// Check to see if this is SSML
TQDomElement 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.
*/
TQStringList SpeechData::parseText(const TQString &text, const TQCString &appId /*=NULL*/)
{
// There has to be a better way
// kdDebug() << "I'm getting: " << endl << text << " from application " << appId << endl;
if (isSsml(text))
{
TQString tempList(text);
return tempList;
}
// See if app has specified a custom sentence delimiter and use it, otherwise use default.
TQRegExp sentenceDelimiter;
if (sentenceDelimiters.find(appId) != sentenceDelimiters.end())
sentenceDelimiter = TQRegExp(sentenceDelimiters[appId]);
else
sentenceDelimiter = TQRegExp("([\\.\\?\\!\\:\\;]\\s)|(\\n *\\n)");
TQString temp = text;
// Replace spaces, tabs, and formfeeds with a single space.
temp.replace(TQRegExp("[ \\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(TQRegExp("\\t +"), "\t");
// Remove trailing spaces.
temp.replace(TQRegExp(" +\\t"), "\t");
// Remove blank lines.
temp.replace(TQRegExp("\t\t+"),"\t");
// Split into sentences.
TQStringList tempList = TQStringList::split("\t", temp, false);
// for ( TQStringList::Iterator it = tempList.begin(); it != tempList.end(); ++it ) {
// kdDebug() << "'" << *it << "'" << endl;
// }
return tempList;
}
/**
* Queues a text job.
*/
uint SpeechData::setText( const TQString &text, const TQString &talker, const TQCString &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
TQStringList tempList = parseText(text, appId);
job->sentences = tempList;
job->partSeqNums.append(tempList.count());
textJobs.append(job);
emit textSet(appId, jobNum);
#else
job->sentences = TQStringList();
job->partSeqNums = TQValueList<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 TQString &text, const uint jobNum, const TQCString& /*appId*/)
{
// kdDebug() << "Running: SpeechData::appendText" << endl;
int newPartNum = 0;
mlJob* job = findJobByJobNum(jobNum);
if (job)
{
job->partCount++;
#if NO_FILTERS
TQStringList 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 TQCString& appId)
{
if (appId == NULL)
return textJobs.getLast();
else
{
TQPtrListIterator<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 TQCString& 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 TQCString& 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)
{
TQPtrListIterator<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().
*/
TQCString SpeechData::getAppIdByJobNum(const uint jobNum)
{
TQCString 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;
TQCString 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.
TQPtrListIterator<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 TQPair<TQCString, uint> removedJob;
typedef TQValueList<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)
{
TQCString 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 TQString &delimiter, const TQCString 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.
*/
TQString SpeechData::getTextJobNumbers()
{
TQString jobs;
TQPtrListIterator<mlJob> it(textJobs);
for ( ; it.current(); ++it )
{
if (!jobs.isEmpty()) jobs.append(",");
jobs.append(TQString::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 TQDataStream containing information about the job.
* Blank if no such job.
*
* The stream contains the following elements:
* - int state Job state.
* - TQCString appId DCOP senderId of the application that requested the speech job.
* - TQString 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
TQByteArray jobInfo = getTextJobInfo(jobNum);
TQDataStream stream(jobInfo, IO_ReadOnly);
int state;
TQCString appId;
TQString talker;
int seq;
int sentenceCount;
int partNum;
int partCount;
stream >> state;
stream >> appId;
stream >> talker;
stream >> seq;
stream >> sentenceCount;
stream >> partNum;
stream >> partCount;
@endverbatim
*/
TQByteArray SpeechData::getTextJobInfo(const uint jobNum)
{
mlJob* job = findJobByJobNum(jobNum);
TQByteArray temp;
if (job)
{
waitJobFiltering(job);
TQDataStream 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 "".
*/
TQString SpeechData::getTextJobSentence(const uint jobNum, const uint seq /*=1*/)
{
mlJob* job = findJobByJobNum(jobNum);
TQString 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 TQString &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 TQString& 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;
TQPtrListIterator<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, TQT_SIGNAL(filteringFinished()), this, TQT_SLOT(slotFilterMgrFinished()));
connect (filterMgr, TQT_SIGNAL(filteringStopped()), this, TQT_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;
TQPtrListIterator<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;
TQPtrListIterator<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.
TQString 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.
TQStringList sentences = TQStringList::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();
}