/*
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 , TQ_SIGNAL ( filteringFinished ( ) ) , this , TQ_SLOT ( slotFilterMgrFinished ( ) ) ) ;
connect ( filterMgr , TQ_SIGNAL ( filteringStopped ( ) ) , this , TQ_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 , TQ_SIGNAL ( filteringFinished ( ) ) , this , TQ_SLOT ( slotFilterMgrFinished ( ) ) ) ;
connect ( filterMgr , TQ_SIGNAL ( filteringStopped ( ) ) , this , TQ_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 ( ) ;
}