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.
1702 lines
65 KiB
1702 lines
65 KiB
/***************************************************** vim:set ts=4 sw=4 sts=4:
|
|
Speaker class.
|
|
This class is in charge of getting the messages, warnings and text from
|
|
the queue and call the plug ins function to actually speak the texts.
|
|
-------------------
|
|
Copyright:
|
|
(C) 2002-2003 by José Pablo Ezequiel "Pupeno" Fernández <pupeno@kde.org>
|
|
(C) 2003-2004 by Olaf Schmidt <ojschmidt@kde.org>
|
|
(C) 2004 by Gary Cramblitt <garycramblitt@comcast.net>
|
|
-------------------
|
|
Original author: José Pablo Ezequiel "Pupeno" Fernández
|
|
******************************************************************************/
|
|
|
|
/******************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License. *
|
|
* *
|
|
******************************************************************************/
|
|
|
|
// TQt includes.
|
|
#include <tqfile.h>
|
|
#include <tqtimer.h>
|
|
#include <tqdir.h>
|
|
|
|
// KDE includes.
|
|
#include <kdebug.h>
|
|
#include <tdelocale.h>
|
|
#include <tdeparts/componentfactory.h>
|
|
#include <ktrader.h>
|
|
#include <tdeapplication.h>
|
|
#include <kstandarddirs.h>
|
|
#include <tdetempfile.h>
|
|
//#include <tdeio/job.h>
|
|
|
|
// KTTSD includes.
|
|
#include "player.h"
|
|
#include "speaker.h"
|
|
#include "speaker.moc"
|
|
#include "talkermgr.h"
|
|
#include "utils.h"
|
|
|
|
/**
|
|
* The Speaker class takes sentences from the text queue, messages from the
|
|
* messages queue, warnings from the warnings queue, and Screen Reader
|
|
* output and places them into an internal "utterance queue". It then
|
|
* loops through this queue, farming the work off to the plugins.
|
|
* It tries to optimize processing so as to keep the plugins busy as
|
|
* much as possible, while ensuring that only one stream of audio is
|
|
* heard at any one time.
|
|
*
|
|
* The message queues are maintained in the SpeechData class.
|
|
*
|
|
* Text jobs in the text queue each have a state (queued, speakable,
|
|
* speaking, paused, finished). Each plugin has a state (idle, saying, synthing,
|
|
* or finished). And finally, each utterance has a state (waiting, saying,
|
|
* synthing, playing, finished). It can be confusing if you are not aware
|
|
* of all these states.
|
|
*
|
|
* Speaker takes some pains to ensure speech is spoken in the correct order,
|
|
* namely Screen Reader Output has the highest priority, Warnings are next,
|
|
* Messages are next, and finally regular text jobs. Since Screen Reader
|
|
* Output, Warnings, and Messages can be queued in the middle of a text
|
|
* job, Speaker must be prepared to reorder utterances in its queue.
|
|
*
|
|
* At the same time, it must issue the signals to inform programs
|
|
* what is happening.
|
|
*
|
|
* Finally, users can pause, restart, delete, advance, or rewind text jobs
|
|
* and Speaker must respond to these commands. In some cases, utterances that
|
|
* have already been synthesized and are ready for audio output must be
|
|
* discarded in response to these commands.
|
|
*
|
|
* Some general guidelines for programmers modifying this code:
|
|
* - Avoid blocking at all cost. If a plugin won't stopText, keep going.
|
|
* You might have to wait for the plugin to complete before asking it
|
|
* to perform the next operation, but in the meantime, there might be
|
|
* other useful work that can be performed.
|
|
* - In no case allow the main thread TQt event loop to block.
|
|
* - Plugins that do not have asynchronous support are wrapped in the
|
|
* ThreadedPlugin class, which attempts to make them as asynchronous as
|
|
* it can, but there are limits.
|
|
* - doUtterances is the main worker method. If in doubt, call doUtterances.
|
|
* - Because Speaker controls the ordering of utterances, most sequence-related
|
|
* signals must be emitted by Speaker; not SpeechData. Only the add
|
|
* and delete job-related signals eminate from SpeechData.
|
|
* - The states of the 3 types of objects mentioned above (jobs, utterances,
|
|
* and plugins) can interact in subtle ways. Test fully. For example, while
|
|
* a text job might be in a paused state, the plugin could be synthesizing
|
|
* in anticipation of resume, or sythesizing a Warning, Message, or
|
|
* Screen Reader Output. Meanwhile, while one of the utterances might
|
|
* have a paused state, others from the same job could be synthing, waiting,
|
|
* or finished.
|
|
* - There can be more than one Audio Player object in existence at one time, although
|
|
* there must never be more than one actually playing at one time. For
|
|
* example, an Audio Player playing an utterance from a text job can be
|
|
* in a paused state, while another Audio Player is playing a Screen Reader
|
|
* Output utterance. Finally, since some plugins do their own audio, it
|
|
* might be that none of the Audio Player objects are playing.
|
|
*/
|
|
|
|
/* Public Methods ==========================================================*/
|
|
|
|
/**
|
|
* Constructor.
|
|
* Loads plugins.
|
|
*/
|
|
Speaker::Speaker( SpeechData*speechData, TalkerMgr* talkerMgr,
|
|
TQObject *parent, const char *name) :
|
|
TQObject(parent, name),
|
|
m_speechData(speechData),
|
|
m_talkerMgr(talkerMgr)
|
|
{
|
|
// kdDebug() << "Running: Speaker::Speaker()" << endl;
|
|
m_exitRequested = false;
|
|
m_textInterrupted = false;
|
|
m_currentJobNum = 0;
|
|
m_lastAppId = 0;
|
|
m_lastJobNum = 0;
|
|
m_lastSeq = 0;
|
|
m_timer = new TQTimer(this, "kttsdAudioTimer");
|
|
m_speechData->config->setGroup("General");
|
|
m_playerOption = m_speechData->config->readNumEntry("AudioOutputMethod", 0); // default to aRts.
|
|
// Map 50% to 100% onto 2.0 to 0.5.
|
|
m_audioStretchFactor = 1.0/(float(m_speechData->config->readNumEntry("AudioStretchFactor", 100))/100.0);
|
|
switch (m_playerOption)
|
|
{
|
|
case 0: break;
|
|
case 1:
|
|
m_speechData->config->setGroup("GStreamerPlayer");
|
|
m_sinkName = m_speechData->config->readEntry("SinkName", "osssink");
|
|
m_periodSize = m_speechData->config->readNumEntry("PeriodSize", 128);
|
|
m_periods = m_speechData->config->readNumEntry("Periods", 8);
|
|
m_playerDebugLevel = m_speechData->config->readNumEntry("DebugLevel", 1);
|
|
break;
|
|
case 2:
|
|
m_speechData->config->setGroup("ALSAPlayer");
|
|
m_sinkName = m_speechData->config->readEntry("PcmName", "default");
|
|
if ("custom" == m_sinkName)
|
|
m_sinkName = m_speechData->config->readEntry("CustomPcmName", "default");
|
|
m_periodSize = m_speechData->config->readNumEntry("PeriodSize", 128);
|
|
m_periods = m_speechData->config->readNumEntry("Periods", 8);
|
|
m_playerDebugLevel = m_speechData->config->readNumEntry("DebugLevel", 1);
|
|
break;
|
|
case 3:
|
|
m_speechData->config->setGroup("aKodePlayer");
|
|
m_sinkName = m_speechData->config->readEntry("SinkName", "auto");
|
|
m_periodSize = m_speechData->config->readNumEntry("PeriodSize", 128);
|
|
m_periods = m_speechData->config->readNumEntry("Periods", 8);
|
|
m_playerDebugLevel = m_speechData->config->readNumEntry("DebugLevel", 1);
|
|
break;
|
|
}
|
|
// Connect timer timeout signal.
|
|
connect(m_timer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotTimeout()));
|
|
|
|
// Connect plugins to slots.
|
|
TQPtrList<PlugInProc> plugins = m_talkerMgr->getLoadedPlugIns();
|
|
const int pluginsCount = plugins.count();
|
|
for (int ndx = 0; ndx < pluginsCount; ++ndx)
|
|
{
|
|
PlugInProc* speech = plugins.at(ndx);
|
|
connect(speech, TQT_SIGNAL(synthFinished()),
|
|
this, TQT_SLOT(slotSynthFinished()));
|
|
connect(speech, TQT_SIGNAL(sayFinished()),
|
|
this, TQT_SLOT(slotSayFinished()));
|
|
connect(speech, TQT_SIGNAL(stopped()),
|
|
this, TQT_SLOT(slotStopped()));
|
|
connect(speech, TQT_SIGNAL(error(bool, const TQString&)),
|
|
this, TQT_SLOT(slotError(bool, const TQString&)));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destructor.
|
|
*/
|
|
Speaker::~Speaker(){
|
|
// kdDebug() << "Running: Speaker::~Speaker()" << endl;
|
|
m_timer->stop();
|
|
delete m_timer;
|
|
if (!m_uttQueue.isEmpty())
|
|
{
|
|
uttIterator it;
|
|
for (it = m_uttQueue.begin(); it != m_uttQueue.end(); )
|
|
it = deleteUtterance(it);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tells the speaker it is requested to exit.
|
|
* TODO: I don't think this actually accomplishes anything.
|
|
*/
|
|
void Speaker::requestExit(){
|
|
// kdDebug() << "Speaker::requestExit: Running" << endl;
|
|
m_exitRequested = true;
|
|
}
|
|
|
|
/**
|
|
* Main processing loop. Dequeues utterances and sends them to the
|
|
* plugins and/or Audio Player.
|
|
*/
|
|
void Speaker::doUtterances()
|
|
{
|
|
// kdDebug() << "Running: Speaker::doUtterances()" << endl;
|
|
|
|
// Used to prevent exiting prematurely.
|
|
m_again = true;
|
|
|
|
while(m_again && !m_exitRequested)
|
|
{
|
|
m_again = false;
|
|
|
|
if (m_exitRequested)
|
|
{
|
|
// kdDebug() << "Speaker::run: exiting due to request 1." << endl;
|
|
return;
|
|
}
|
|
|
|
uttIterator it;
|
|
uttIterator itBegin;
|
|
uttIterator itEnd = 0; // Init to zero to avoid compiler warning.
|
|
|
|
// If Screen Reader Output is waiting, we need to process it ASAP.
|
|
if (m_speechData->screenReaderOutputReady())
|
|
{
|
|
m_again = getNextUtterance();
|
|
}
|
|
// kdDebug() << "Speaker::doUtterances: queue dump:" << endl;
|
|
// for (it = m_uttQueue.begin(); it != m_uttQueue.end(); ++it)
|
|
// {
|
|
// TQString pluginState = "no plugin";
|
|
// if (it->plugin) pluginState = pluginStateToStr(it->plugin->getState());
|
|
// TQString jobState =
|
|
// jobStateToStr(m_speechData->getTextJobState(it->sentence->jobNum));
|
|
// kdDebug() <<
|
|
// " State: " << uttStateToStr(it->state) <<
|
|
// "," << pluginState <<
|
|
// "," << jobState <<
|
|
// " Type: " << uttTypeToStr(it->utType) <<
|
|
// " Text: " << it->sentence->text << endl;
|
|
// }
|
|
|
|
if (!m_uttQueue.isEmpty())
|
|
{
|
|
// Delete utterances that are finished.
|
|
it = m_uttQueue.begin();
|
|
while (it != m_uttQueue.end())
|
|
{
|
|
if (it->state == usFinished)
|
|
it = deleteUtterance(it);
|
|
else
|
|
++it;
|
|
}
|
|
// Loop through utterance queue.
|
|
int waitingCnt = 0;
|
|
int waitingMsgCnt = 0;
|
|
int transformingCnt = 0;
|
|
bool playing = false;
|
|
int synthingCnt = 0;
|
|
itEnd = m_uttQueue.end();
|
|
itBegin = m_uttQueue.begin();
|
|
for (it = itBegin; it != itEnd; ++it)
|
|
{
|
|
uttState utState = it->state;
|
|
uttType utType = it->utType;
|
|
switch (utState)
|
|
{
|
|
case usNone:
|
|
{
|
|
setInitialUtteranceState(*it);
|
|
m_again = true;
|
|
break;
|
|
}
|
|
case usWaitingTransform:
|
|
{
|
|
// Create an XSLT transformer and transform the text.
|
|
it->transformer = new SSMLConvert();
|
|
connect(it->transformer, TQT_SIGNAL(transformFinished()),
|
|
this, TQT_SLOT(slotTransformFinished()));
|
|
if (it->transformer->transform(it->sentence->text,
|
|
it->plugin->getSsmlXsltFilename()))
|
|
{
|
|
it->state = usTransforming;
|
|
++transformingCnt;
|
|
}
|
|
else
|
|
{
|
|
// If an error occurs transforming, skip it.
|
|
it->state = usTransforming;
|
|
setInitialUtteranceState(*it);
|
|
}
|
|
m_again = true;
|
|
break;
|
|
}
|
|
case usTransforming:
|
|
{
|
|
// See if transformer is finished.
|
|
if (it->transformer->getState() == SSMLConvert::tsFinished)
|
|
{
|
|
// Get the transformed text.
|
|
it->sentence->text = it->transformer->getOutput();
|
|
// Set next state (usWaitingSynth or usWaitingSay)
|
|
setInitialUtteranceState(*it);
|
|
m_again = true;
|
|
--transformingCnt;
|
|
}
|
|
break;
|
|
}
|
|
case usWaitingSignal:
|
|
{
|
|
// If first in queue, emit signal.
|
|
if (it == itBegin)
|
|
{
|
|
if (utType == utStartOfJob)
|
|
{
|
|
m_speechData->setTextJobState(
|
|
it->sentence->jobNum, KSpeech::jsSpeaking);
|
|
if (it->sentence->seq == 0)
|
|
emit textStarted(it->sentence->appId,
|
|
it->sentence->jobNum);
|
|
else
|
|
emit textResumed(it->sentence->appId,
|
|
it->sentence->jobNum);
|
|
} else {
|
|
m_speechData->setTextJobState(
|
|
it->sentence->jobNum, KSpeech::jsFinished);
|
|
emit textFinished(it->sentence->appId, it->sentence->jobNum);
|
|
}
|
|
it->state = usFinished;
|
|
m_again = true;
|
|
}
|
|
break;
|
|
}
|
|
case usSynthed:
|
|
{
|
|
// Don't bother stretching if factor is 1.0.
|
|
// Don't bother stretching if SSML.
|
|
// TODO: This is because sox mangles SSML pitch settings. Would be nice
|
|
// to figure out how to avoid this.
|
|
if (m_audioStretchFactor == 1.0 || it->isSSML)
|
|
{
|
|
it->state = usStretched;
|
|
m_again = true;
|
|
}
|
|
else
|
|
{
|
|
it->audioStretcher = new Stretcher();
|
|
connect(it->audioStretcher, TQT_SIGNAL(stretchFinished()),
|
|
this, TQT_SLOT(slotStretchFinished()));
|
|
if (it->audioStretcher->stretch(it->audioUrl, makeSuggestedFilename(),
|
|
m_audioStretchFactor))
|
|
{
|
|
it->state = usStretching;
|
|
m_again = true; // Is this needed?
|
|
}
|
|
else
|
|
{
|
|
// If stretch failed, it is most likely caused by sox not being
|
|
// installed. Just skip it.
|
|
it->state = usStretched;
|
|
m_again = true;
|
|
delete it->audioStretcher;
|
|
it->audioStretcher= 0;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case usStretching:
|
|
{
|
|
// See if Stretcher is finished.
|
|
if (it->audioStretcher->getState() == Stretcher::ssFinished)
|
|
{
|
|
TQFile::remove(it->audioUrl);
|
|
it->audioUrl = it->audioStretcher->getOutFilename();
|
|
it->state = usStretched;
|
|
delete it->audioStretcher;
|
|
it->audioStretcher = 0;
|
|
m_again = true;
|
|
}
|
|
break;
|
|
}
|
|
case usStretched:
|
|
{
|
|
// If first in queue, start playback.
|
|
if (it == itBegin)
|
|
{
|
|
if (startPlayingUtterance(it))
|
|
{
|
|
playing = true;
|
|
m_again = true;
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
break;
|
|
}
|
|
case usPlaying:
|
|
{
|
|
playing = true;
|
|
break;
|
|
}
|
|
case usPaused:
|
|
case usPreempted:
|
|
{
|
|
if (!playing)
|
|
{
|
|
if (startPlayingUtterance(it))
|
|
{
|
|
playing = true;
|
|
m_again = true;
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
break;
|
|
}
|
|
case usWaitingSay:
|
|
{
|
|
// If first in queue, start it.
|
|
if (it == itBegin)
|
|
{
|
|
int jobState =
|
|
m_speechData->getTextJobState(it->sentence->jobNum);
|
|
if ((jobState == KSpeech::jsSpeaking) ||
|
|
(jobState == KSpeech::jsSpeakable))
|
|
{
|
|
if (it->plugin->getState() == psIdle)
|
|
{
|
|
// Set job to speaking state and set sequence number.
|
|
mlText* sentence = it->sentence;
|
|
m_currentJobNum = sentence->jobNum;
|
|
m_speechData->setTextJobState(m_currentJobNum, KSpeech::jsSpeaking);
|
|
m_speechData->setJobSequenceNum(m_currentJobNum, sentence->seq);
|
|
prePlaySignals(it);
|
|
// kdDebug() << "Async synthesis and audibilizing." << endl;
|
|
it->state = usSaying;
|
|
playing = true;
|
|
it->plugin->sayText(it->sentence->text);
|
|
m_again = true;
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
} else {
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
break;
|
|
}
|
|
case usWaitingSynth:
|
|
{
|
|
// TODO: If the synth is busy and the waiting text is screen
|
|
// reader output, it would be nice to call the synth's
|
|
// stopText() method. However, some of the current plugins
|
|
// have horrible startup times, so we won't do that for now.
|
|
if (it->plugin->getState() == psIdle)
|
|
{
|
|
// kdDebug() << "Async synthesis." << endl;
|
|
it->state = usSynthing;
|
|
++synthingCnt;
|
|
it->plugin->synthText(it->sentence->text,
|
|
makeSuggestedFilename());
|
|
m_again = true;
|
|
}
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
break;
|
|
}
|
|
case usSaying:
|
|
{
|
|
// See if synthesis and audibilizing is finished.
|
|
if (it->plugin->getState() == psFinished)
|
|
{
|
|
it->plugin->ackFinished();
|
|
it->state = usFinished;
|
|
m_again = true;
|
|
} else {
|
|
playing = true;
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
}
|
|
break;
|
|
}
|
|
case usSynthing:
|
|
{
|
|
// See if synthesis is completed.
|
|
if (it->plugin->getState() == psFinished)
|
|
{
|
|
it->audioUrl = TDEStandardDirs::realFilePath(it->plugin->getFilename());
|
|
// kdDebug() << "Speaker::doUtterances: synthesized filename: " << it->audioUrl << endl;
|
|
it->plugin->ackFinished();
|
|
it->state = usSynthed;
|
|
m_again = true;
|
|
} else ++synthingCnt;
|
|
++waitingCnt;
|
|
if (utType == utWarning || utType == utMessage) ++waitingMsgCnt;
|
|
break;
|
|
}
|
|
case usFinished: break;
|
|
}
|
|
}
|
|
// See if there are any messages or warnings to process.
|
|
// We keep up to 2 such utterances in the queue.
|
|
if ((waitingMsgCnt < 2) && (transformingCnt < 3))
|
|
{
|
|
if (m_speechData->warningInQueue() || m_speechData->messageInQueue())
|
|
{
|
|
getNextUtterance();
|
|
m_again = true;
|
|
}
|
|
}
|
|
// Try to keep at least two utterances in the queue waiting to be played,
|
|
// and no more than 3 transforming at one time.
|
|
if ((waitingCnt < 2) && (transformingCnt < 3))
|
|
if (getNextUtterance()) m_again = true;
|
|
} else {
|
|
// See if another utterance is ready to be worked on.
|
|
// If so, loop again since we've got work to do.
|
|
m_again = getNextUtterance();
|
|
}
|
|
}
|
|
// kdDebug() << "Speaker::doUtterances: exiting." << endl;
|
|
}
|
|
|
|
/**
|
|
* Determine if kttsd is currently speaking any text jobs.
|
|
* @return True if currently speaking any text jobs.
|
|
*/
|
|
bool Speaker::isSpeakingText()
|
|
{
|
|
return (m_speechData->getTextJobState(m_currentJobNum) == KSpeech::jsSpeaking);
|
|
}
|
|
|
|
/**
|
|
* Get the job number of the current text job.
|
|
* @return Job number of the current text job. 0 if no jobs.
|
|
*
|
|
* Note that the current job may not be speaking. See @ref isSpeakingText.
|
|
*/
|
|
uint Speaker::getCurrentTextJob() { return m_currentJobNum; }
|
|
|
|
/**
|
|
* Remove a text job from the queue.
|
|
* @param jobNum Job number of the text job.
|
|
*
|
|
* The job is deleted from the queue and the @ref textRemoved signal is emitted.
|
|
*
|
|
* If there is another job in the text queue, and it is marked speakable,
|
|
* that job begins speaking.
|
|
*/
|
|
void Speaker::removeText(const uint jobNum)
|
|
{
|
|
deleteUtteranceByJobNum(jobNum);
|
|
m_speechData->removeText(jobNum);
|
|
doUtterances();
|
|
}
|
|
|
|
/**
|
|
* Start a text job at the beginning.
|
|
* @param jobNum Job number of the text job.
|
|
*
|
|
* Rewinds the job to the beginning.
|
|
*
|
|
* The job is marked speakable.
|
|
* If there are other speakable jobs preceeding this one in the queue,
|
|
* those jobs continue speaking and when finished, this job will begin speaking.
|
|
* If there are no other speakable jobs preceeding this one, it begins speaking.
|
|
*
|
|
* The @ref textStarted signal is emitted when the text job begins speaking.
|
|
* When all the sentences of the job have been spoken, the job is marked for deletion from
|
|
* the text queue and the @ref textFinished signal is emitted.
|
|
*/
|
|
void Speaker::startText(const uint jobNum)
|
|
{
|
|
deleteUtteranceByJobNum(jobNum);
|
|
m_speechData->setJobSequenceNum(jobNum, 1);
|
|
m_speechData->setTextJobState(jobNum, KSpeech::jsSpeakable);
|
|
if (m_lastJobNum == jobNum)
|
|
{
|
|
// kdDebug() << "Speaker::startText: startText called on speaking job " << jobNum << endl;
|
|
m_lastJobNum = 0;
|
|
m_lastAppId = 0;
|
|
m_lastSeq = 0;
|
|
}
|
|
doUtterances();
|
|
}
|
|
|
|
/**
|
|
* Stop a text job and rewind to the beginning.
|
|
* @param jobNum Job number of the text job.
|
|
*
|
|
* The job is marked not speakable and will not be speakable until @ref startText or @ref resumeText
|
|
* is called.
|
|
*
|
|
* If there are speaking jobs preceeding this one in the queue, they continue speaking.
|
|
* If the job is currently speaking, the @ref textStopped signal is emitted and the job stops speaking.
|
|
* Depending upon the speech engine and plugin used, speeking may not stop immediately
|
|
* (it might finish the current sentence).
|
|
*/
|
|
void Speaker::stopText(const uint jobNum)
|
|
{
|
|
bool emitSignal = (m_speechData->getTextJobState(jobNum) == KSpeech::jsSpeaking);
|
|
deleteUtteranceByJobNum(jobNum);
|
|
m_speechData->setJobSequenceNum(jobNum, 1);
|
|
m_speechData->setTextJobState(jobNum, KSpeech::jsQueued);
|
|
if (emitSignal) textStopped(m_speechData->getAppIdByJobNum(jobNum), jobNum);
|
|
// Call doUtterances to process other jobs.
|
|
doUtterances();
|
|
}
|
|
|
|
/**
|
|
* Pause a text job.
|
|
* @param jobNum Job number of the text job.
|
|
*
|
|
* The job is marked as paused and will not be speakable until @ref resumeText or
|
|
* @ref startText is called.
|
|
*
|
|
* If there are speaking jobs preceeding this one in the queue, they continue speaking.
|
|
* If the job is currently speaking, the @ref textPaused signal is emitted and the job stops speaking.
|
|
* Depending upon the speech engine and plugin used, speeking may not stop immediately
|
|
* (it might finish the current sentence).
|
|
* @see resumeText
|
|
*/
|
|
void Speaker::pauseText(const uint jobNum)
|
|
{
|
|
bool emitSignal = (m_speechData->getTextJobState(jobNum) == KSpeech::jsSpeaking);
|
|
pauseUtteranceByJobNum(jobNum);
|
|
kdDebug() << "Speaker::pauseText: setting Job State of job " << jobNum << " to jsPaused" << endl;
|
|
m_speechData->setTextJobState(jobNum, KSpeech::jsPaused);
|
|
if (emitSignal) textPaused(m_speechData->getAppIdByJobNum(jobNum),jobNum);
|
|
}
|
|
|
|
/**
|
|
* Start or resume a text job where it was paused.
|
|
* @param jobNum Job number of the text job.
|
|
*
|
|
* The job is marked speakable.
|
|
*
|
|
* If the job is currently speaking, or is waiting to be spoken (speakable
|
|
* state), the resumeText() call is ignored.
|
|
*
|
|
* If the job is currently queued, or is finished, it is the same as calling
|
|
* @ref startText .
|
|
*
|
|
* If there are speaking jobs preceeding this one in the queue, those jobs continue speaking and,
|
|
* when finished this job will begin speaking where it left off.
|
|
*
|
|
* The @ref textResumed signal is emitted when the job resumes.
|
|
* @see pauseText
|
|
*/
|
|
void Speaker::resumeText(const uint jobNum)
|
|
{
|
|
int state = m_speechData->getTextJobState(jobNum);
|
|
switch (state)
|
|
{
|
|
case KSpeech::jsQueued:
|
|
case KSpeech::jsFinished:
|
|
startText(jobNum);
|
|
break;
|
|
case KSpeech::jsSpeakable:
|
|
case KSpeech::jsSpeaking:
|
|
doUtterances();
|
|
break;
|
|
case KSpeech::jsPaused:
|
|
if (jobNum == m_currentJobNum)
|
|
m_speechData->setTextJobState(jobNum, KSpeech::jsSpeaking);
|
|
else
|
|
m_speechData->setTextJobState(jobNum, KSpeech::jsSpeakable);
|
|
doUtterances();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move a text job down in the queue so that it is spoken later.
|
|
* @param jobNum Job number of the text job.
|
|
*
|
|
* If the job is currently speaking, it is paused.
|
|
* If the next job in the queue is speakable, it begins speaking.
|
|
*/
|
|
void Speaker::moveTextLater(const uint jobNum)
|
|
{
|
|
if (m_speechData->getTextJobState(jobNum) == KSpeech::jsSpeaking)
|
|
m_speechData->setTextJobState(jobNum, KSpeech::jsPaused);
|
|
deleteUtteranceByJobNum(jobNum);
|
|
m_speechData->moveTextLater(jobNum);
|
|
doUtterances();
|
|
}
|
|
|
|
/**
|
|
* Jump to the first sentence of a specified part of a text job.
|
|
* @param partNum Part number of the part to jump to. Parts are numbered starting at 1.
|
|
* @param jobNum Job number of the text job.
|
|
* @return Part number of the part actually jumped to.
|
|
*
|
|
* If partNum is greater than the number of parts in the job, jumps to last part.
|
|
* If partNum is 0, does nothing and returns the current part number.
|
|
* If no such job, does nothing and returns 0.
|
|
* Does not affect the current speaking/not-speaking state of the job.
|
|
*/
|
|
int Speaker::jumpToTextPart(const int partNum, const uint jobNum)
|
|
{
|
|
if (partNum == 0) return m_speechData->jumpToTextPart(partNum, jobNum);
|
|
deleteUtteranceByJobNum(jobNum);
|
|
int pNum = m_speechData->jumpToTextPart(partNum, jobNum);
|
|
if (pNum)
|
|
{
|
|
uint seq = m_speechData->getJobSequenceNum(jobNum);
|
|
if (jobNum == m_lastJobNum)
|
|
{
|
|
if (seq == 0)
|
|
m_lastSeq = seq;
|
|
else
|
|
m_lastSeq = seq - 1;
|
|
}
|
|
if (jobNum == m_currentJobNum)
|
|
{
|
|
m_lastJobNum = jobNum;
|
|
if (seq == 0)
|
|
m_lastSeq = 0;
|
|
else
|
|
m_lastSeq = seq - 1;
|
|
doUtterances();
|
|
}
|
|
}
|
|
return pNum;
|
|
}
|
|
|
|
/**
|
|
* Advance or rewind N sentences in a text job.
|
|
* @param n Number of sentences to advance (positive) or rewind (negative)
|
|
* in the job.
|
|
* @param jobNum Job number of the text job.
|
|
* @return Sequence number of the sentence actually moved to.
|
|
* Sequence numbers are numbered starting at 1.
|
|
*
|
|
* If no such job, does nothing and returns 0.
|
|
* If n is zero, returns the current sequence number of the job.
|
|
* Does not affect the current speaking/not-speaking state of the job.
|
|
*/
|
|
uint Speaker::moveRelTextSentence(const int n, const uint jobNum)
|
|
{
|
|
if (0 == n)
|
|
return m_speechData->getJobSequenceNum(jobNum);
|
|
else {
|
|
deleteUtteranceByJobNum(jobNum);
|
|
// TODO: More efficient way to advance one or two sentences, since there is a
|
|
// good chance those utterances are already in the queue and synthesized.
|
|
uint seq = m_speechData->moveRelTextSentence(n, jobNum);
|
|
kdDebug() << "Speaker::moveRelTextSentence: job num: " << jobNum << " moved to seq: " << seq << endl;
|
|
if (jobNum == m_lastJobNum)
|
|
{
|
|
if (seq == 0)
|
|
m_lastSeq = seq;
|
|
else
|
|
m_lastSeq = seq - 1;
|
|
}
|
|
if (jobNum == m_currentJobNum)
|
|
{
|
|
m_lastJobNum = jobNum;
|
|
if (seq == 0)
|
|
m_lastSeq = 0;
|
|
else
|
|
m_lastSeq = seq - 1;
|
|
doUtterances();
|
|
}
|
|
return seq;
|
|
}
|
|
}
|
|
|
|
/* Private Methods ==========================================================*/
|
|
|
|
/**
|
|
* Converts an utterance state enumerator to a displayable string.
|
|
* @param state Utterance state.
|
|
*/
|
|
TQString Speaker::uttStateToStr(uttState state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case usNone: return "usNone";
|
|
case usWaitingTransform: return "usWaitingTransform";
|
|
case usTransforming: return "usTransforming";
|
|
case usWaitingSay: return "usWaitingSay";
|
|
case usWaitingSynth: return "usWaitingSynth";
|
|
case usWaitingSignal: return "usWaitingSignal";
|
|
case usSaying: return "usSaying";
|
|
case usSynthing: return "usSynthing";
|
|
case usSynthed: return "usSynthed";
|
|
case usStretching: return "usStretching";
|
|
case usStretched: return "usStretched";
|
|
case usPlaying: return "usPlaying";
|
|
case usPaused: return "usPaused";
|
|
case usPreempted: return "usPreempted";
|
|
case usFinished: return "usFinished";
|
|
}
|
|
return TQString();
|
|
}
|
|
|
|
/**
|
|
* Converts an utterance type enumerator to a displayable string.
|
|
* @param utType Utterance type.
|
|
* @return Displayable string for utterance type.
|
|
*/
|
|
TQString Speaker::uttTypeToStr(uttType utType)
|
|
{
|
|
switch (utType)
|
|
{
|
|
case utText: return "utText";
|
|
case utInterruptMsg: return "utInterruptMsg";
|
|
case utInterruptSnd: return "utInterruptSnd";
|
|
case utResumeMsg: return "utResumeMsg";
|
|
case utResumeSnd: return "utResumeSnd";
|
|
case utMessage: return "utMessage";
|
|
case utWarning: return "utWarning";
|
|
case utScreenReader: return "utScreenReader";
|
|
case utStartOfJob: return "utStartOfJob";
|
|
case utEndOfJob: return "utEndOfJob";
|
|
}
|
|
return TQString();
|
|
}
|
|
|
|
/**
|
|
* Converts a plugin state enumerator to a displayable string.
|
|
* @param state Plugin state.
|
|
* @return Displayable string for plugin state.
|
|
*/
|
|
TQString Speaker::pluginStateToStr(pluginState state)
|
|
{
|
|
switch( state )
|
|
{
|
|
case psIdle: return "psIdle";
|
|
case psSaying: return "psSaying";
|
|
case psSynthing: return "psSynthing";
|
|
case psFinished: return "psFinished";
|
|
}
|
|
return TQString();
|
|
}
|
|
|
|
/**
|
|
* Converts a job state enumerator to a displayable string.
|
|
* @param state Job state.
|
|
* @return Displayable string for job state.
|
|
*/
|
|
TQString Speaker::jobStateToStr(int state)
|
|
{
|
|
switch ( state )
|
|
{
|
|
case KSpeech::jsQueued: return "jsQueued";
|
|
case KSpeech::jsSpeakable: return "jsSpeakable";
|
|
case KSpeech::jsSpeaking: return "jsSpeaking";
|
|
case KSpeech::jsPaused: return "jsPaused";
|
|
case KSpeech::jsFinished: return "jsFinished";
|
|
}
|
|
return TQString();
|
|
}
|
|
|
|
/**
|
|
* Delete any utterances in the queue with this jobNum.
|
|
* @param jobNum Job Number of the utterances to delete.
|
|
* If currently processing any deleted utterances, stop them.
|
|
*/
|
|
void Speaker::deleteUtteranceByJobNum(const uint jobNum)
|
|
{
|
|
uttIterator it = m_uttQueue.begin();
|
|
while (it != m_uttQueue.end())
|
|
{
|
|
if (it->sentence)
|
|
{
|
|
if (it->sentence->jobNum == jobNum)
|
|
it = deleteUtterance(it);
|
|
else
|
|
++it;
|
|
} else
|
|
++it;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pause the utterance with this jobNum if it is playing on the Audio Player.
|
|
* @param jobNum The Job Number of the utterance to pause.
|
|
*/
|
|
void Speaker::pauseUtteranceByJobNum(const uint jobNum)
|
|
{
|
|
uttIterator itEnd = m_uttQueue.end();
|
|
for (uttIterator it = m_uttQueue.begin(); it != itEnd; ++it)
|
|
{
|
|
if (it->sentence) // TODO: Why is this necessary?
|
|
if (it->sentence->jobNum == jobNum)
|
|
if (it->state == usPlaying)
|
|
{
|
|
if (it->audioPlayer)
|
|
if (it->audioPlayer->playing())
|
|
{
|
|
m_timer->stop();
|
|
kdDebug() << "Speaker::pauseUtteranceByJobNum: pausing audio player" << endl;
|
|
it->audioPlayer->pause();
|
|
kdDebug() << "Speaker::pauseUtteranceByJobNum: Setting utterance state to usPaused" << endl;
|
|
it->state = usPaused;
|
|
return;
|
|
}
|
|
// Audio player has finished, but timeout hasn't had a chance
|
|
// to clean up. So do nothing, and let timeout do the cleanup.
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether the given text is SSML markup.
|
|
*/
|
|
bool Speaker::isSsml(const TQString &text)
|
|
{
|
|
return KttsUtils::hasRootElement( text, "speak" );
|
|
}
|
|
|
|
/**
|
|
* Determines the initial state of an utterance. If the utterance contains
|
|
* SSML, the state is set to usWaitingTransform. Otherwise, if the plugin
|
|
* supports async synthesis, sets to usWaitingSynth, otherwise usWaitingSay.
|
|
* If an utterance has already been transformed, usWaitingTransform is
|
|
* skipped to either usWaitingSynth or usWaitingSay.
|
|
* @param utt The utterance.
|
|
*/
|
|
void Speaker::setInitialUtteranceState(Utt &utt)
|
|
{
|
|
if ((utt.state != usTransforming) && utt.isSSML)
|
|
{
|
|
utt.state = usWaitingTransform;
|
|
return;
|
|
}
|
|
if (utt.plugin->supportsSynth())
|
|
utt.state = usWaitingSynth;
|
|
else
|
|
utt.state = usWaitingSay;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given job and sequence number are already in the utterance queue.
|
|
*/
|
|
bool Speaker::isInUtteranceQueue(uint jobNum, uint seqNum)
|
|
{
|
|
uttIterator itEnd = m_uttQueue.end();
|
|
for (uttIterator it = m_uttQueue.begin(); it != itEnd; ++it)
|
|
{
|
|
if (it->sentence)
|
|
{
|
|
if (it->sentence->jobNum == jobNum && it->sentence->seq == seqNum) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets the next utterance to be spoken from speechdata and adds it to the queue.
|
|
* @return True if one or more utterances were added to the queue.
|
|
*
|
|
* Checks for waiting ScreenReaderOutput, Warnings, Messages, or Text,
|
|
* in that order.
|
|
* If Warning or Message and interruption messages have been configured,
|
|
* adds those to the queue as well.
|
|
* Determines which plugin should be used for the utterance.
|
|
*/
|
|
bool Speaker::getNextUtterance()
|
|
{
|
|
bool gotOne = false;
|
|
Utt* utt = 0;
|
|
if (m_speechData->screenReaderOutputReady()) {
|
|
utt = new Utt;
|
|
utt->utType = utScreenReader;
|
|
utt->sentence = m_speechData->getScreenReaderOutput();
|
|
} else {
|
|
if (m_speechData->warningInQueue()) {
|
|
utt = new Utt;
|
|
utt->utType = utWarning;
|
|
utt->sentence = m_speechData->dequeueWarning();
|
|
} else {
|
|
if (m_speechData->messageInQueue()) {
|
|
utt = new Utt;
|
|
utt->utType = utMessage;
|
|
utt->sentence = m_speechData->dequeueMessage();
|
|
} else {
|
|
uint jobNum = m_lastJobNum;
|
|
uint seq = m_lastSeq;
|
|
mlText* sentence = m_speechData->getNextSentenceText(jobNum, seq);
|
|
// Skip over blank lines.
|
|
while (sentence && sentence->text.isEmpty())
|
|
{
|
|
jobNum = sentence->jobNum;
|
|
seq = sentence->seq;
|
|
sentence = m_speechData->getNextSentenceText(jobNum, seq);
|
|
}
|
|
// If this utterance is already in the queue, it means we have run out of
|
|
// stuff to say and are trying to requeue already queued (and waiting stuff).
|
|
if (sentence && !isInUtteranceQueue(sentence->jobNum, sentence->seq))
|
|
{
|
|
utt = new Utt;
|
|
utt->utType = utText;
|
|
utt->sentence = sentence;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (utt)
|
|
{
|
|
gotOne = true;
|
|
utt->isSSML = isSsml(utt->sentence->text);
|
|
utt->state = usNone;
|
|
utt->audioPlayer = 0;
|
|
utt->audioStretcher = 0;
|
|
utt->audioUrl = TQString();
|
|
utt->plugin = m_talkerMgr->talkerToPlugin(utt->sentence->talker);
|
|
// Save some time by setting initial state now.
|
|
setInitialUtteranceState(*utt);
|
|
// Screen Reader Outputs need to be processed ASAP.
|
|
if (utt->utType == utScreenReader)
|
|
{
|
|
m_uttQueue.insert(m_uttQueue.begin(), *utt);
|
|
// Delete any other Screen Reader Outputs in the queue.
|
|
// Only one Screen Reader Output at a time.
|
|
uttIterator it = m_uttQueue.begin();
|
|
++it;
|
|
while (it != m_uttQueue.end())
|
|
{
|
|
if (it->utType == utScreenReader)
|
|
it = deleteUtterance(it);
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
// If the new utterance is a Warning or Message...
|
|
if ((utt->utType == utWarning) || (utt->utType == utMessage))
|
|
{
|
|
uttIterator itEnd = m_uttQueue.end();
|
|
uttIterator it = m_uttQueue.begin();
|
|
bool interrupting = false;
|
|
if (it != itEnd)
|
|
{
|
|
// New Warnings go after Screen Reader Output, other Warnings,
|
|
// Interruptions, and in-process text,
|
|
// but before Resumes, waiting text or signals.
|
|
if (utt->utType == utWarning)
|
|
while ( it != itEnd &&
|
|
((it->utType == utScreenReader) ||
|
|
(it->utType == utWarning) ||
|
|
(it->utType == utInterruptMsg) ||
|
|
(it->utType == utInterruptSnd))) ++it;
|
|
// New Messages go after Screen Reader Output, Warnings, other Messages,
|
|
// Interruptions, and in-process text,
|
|
// but before Resumes, waiting text or signals.
|
|
if (utt->utType == utMessage)
|
|
while ( it != itEnd &&
|
|
((it->utType == utScreenReader) ||
|
|
(it->utType == utWarning) ||
|
|
(it->utType == utMessage) ||
|
|
(it->utType == utInterruptMsg) ||
|
|
(it->utType == utInterruptSnd))) ++it;
|
|
if (it != itEnd)
|
|
if (it->utType == utText &&
|
|
((it->state == usPlaying) ||
|
|
(it->state == usSaying))) ++it;
|
|
// If now pointing at a text message, we are interrupting.
|
|
// Insert optional Interruption message and sound.
|
|
if (it != itEnd) interrupting = (it->utType == utText && it->state != usPaused);
|
|
if (interrupting)
|
|
{
|
|
if (m_speechData->textPreSndEnabled)
|
|
{
|
|
Utt intrUtt;
|
|
intrUtt.sentence = new mlText;
|
|
intrUtt.sentence->text = TQString();
|
|
intrUtt.sentence->talker = utt->sentence->talker;
|
|
intrUtt.sentence->appId = utt->sentence->appId;
|
|
intrUtt.sentence->jobNum = utt->sentence->jobNum;
|
|
intrUtt.sentence->seq = 0;
|
|
intrUtt.audioUrl = m_speechData->textPreSnd;
|
|
intrUtt.audioPlayer = 0;
|
|
intrUtt.utType = utInterruptSnd;
|
|
intrUtt.isSSML = false;
|
|
intrUtt.state = usStretched;
|
|
intrUtt.plugin = 0;
|
|
it = m_uttQueue.insert(it, intrUtt);
|
|
++it;
|
|
}
|
|
if (m_speechData->textPreMsgEnabled)
|
|
{
|
|
Utt intrUtt;
|
|
intrUtt.sentence = new mlText;
|
|
intrUtt.sentence->text = m_speechData->textPreMsg;
|
|
// Interruptions are spoken using default Talker.
|
|
intrUtt.sentence->talker = TQString();
|
|
intrUtt.sentence->appId = utt->sentence->appId;
|
|
intrUtt.sentence->jobNum = utt->sentence->jobNum;
|
|
intrUtt.sentence->seq = 0;
|
|
intrUtt.audioUrl = TQString();
|
|
intrUtt.audioPlayer = 0;
|
|
intrUtt.utType = utInterruptMsg;
|
|
intrUtt.isSSML = isSsml(intrUtt.sentence->text);
|
|
intrUtt.plugin = m_talkerMgr->talkerToPlugin(intrUtt.sentence->talker);
|
|
intrUtt.state = usNone;
|
|
setInitialUtteranceState(intrUtt);
|
|
it = m_uttQueue.insert(it, intrUtt);
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
// Insert the new message or warning.
|
|
it = m_uttQueue.insert(it, *utt);
|
|
++it;
|
|
// Resumption message and sound.
|
|
if (interrupting)
|
|
{
|
|
if (m_speechData->textPostSndEnabled)
|
|
{
|
|
Utt resUtt;
|
|
resUtt.sentence = new mlText;
|
|
resUtt.sentence->text = TQString();
|
|
resUtt.sentence->talker = utt->sentence->talker;
|
|
resUtt.sentence->appId = utt->sentence->appId;
|
|
resUtt.sentence->jobNum = utt->sentence->jobNum;
|
|
resUtt.sentence->seq = 0;
|
|
resUtt.audioUrl = m_speechData->textPostSnd;
|
|
resUtt.audioPlayer = 0;
|
|
resUtt.utType = utResumeSnd;
|
|
resUtt.isSSML = false;
|
|
resUtt.state = usStretched;
|
|
resUtt.plugin = 0;
|
|
it = m_uttQueue.insert(it, resUtt);
|
|
++it;
|
|
}
|
|
if (m_speechData->textPostMsgEnabled)
|
|
{
|
|
Utt resUtt;
|
|
resUtt.sentence = new mlText;
|
|
resUtt.sentence->text = m_speechData->textPostMsg;
|
|
resUtt.sentence->talker = TQString();
|
|
resUtt.sentence->appId = utt->sentence->appId;
|
|
resUtt.sentence->jobNum = utt->sentence->jobNum;
|
|
resUtt.sentence->seq = 0;
|
|
resUtt.audioUrl = TQString();
|
|
resUtt.audioPlayer = 0;
|
|
resUtt.utType = utResumeMsg;
|
|
resUtt.isSSML = isSsml(resUtt.sentence->text);
|
|
resUtt.plugin = m_talkerMgr->talkerToPlugin(resUtt.sentence->talker);
|
|
resUtt.state = usNone;
|
|
setInitialUtteranceState(resUtt);
|
|
it = m_uttQueue.insert(it, resUtt);
|
|
}
|
|
}
|
|
}
|
|
// If a text message...
|
|
if (utt->utType == utText)
|
|
{
|
|
// If job number has changed...
|
|
if (utt->sentence->jobNum != m_lastJobNum)
|
|
{
|
|
// If we finished the last job, append End-of-job to the queue,
|
|
// which will become a textFinished signal when it is processed.
|
|
if (m_lastJobNum)
|
|
{
|
|
if (m_lastSeq == static_cast<uint>(m_speechData->getTextCount(m_lastJobNum)))
|
|
{
|
|
Utt jobUtt;
|
|
jobUtt.sentence = new mlText;
|
|
jobUtt.sentence->text = TQString();
|
|
jobUtt.sentence->talker = TQString();
|
|
jobUtt.sentence->appId = m_lastAppId;
|
|
jobUtt.sentence->jobNum = m_lastJobNum;
|
|
jobUtt.sentence->seq = 0;
|
|
jobUtt.audioUrl = TQString();
|
|
jobUtt.utType = utEndOfJob;
|
|
jobUtt.isSSML = false;
|
|
jobUtt.plugin = 0;
|
|
jobUtt.state = usWaitingSignal;
|
|
m_uttQueue.append(jobUtt);
|
|
}
|
|
}
|
|
m_lastJobNum = utt->sentence->jobNum;
|
|
m_lastAppId = utt->sentence->appId;
|
|
// If we are at beginning of new job, append Start-of-job to queue,
|
|
// which will become a textStarted signal when it is processed.
|
|
if (utt->sentence->seq == 1)
|
|
{
|
|
Utt jobUtt;
|
|
jobUtt.sentence = new mlText;
|
|
jobUtt.sentence->text = TQString();
|
|
jobUtt.sentence->talker = TQString();
|
|
jobUtt.sentence->appId = m_lastAppId;
|
|
jobUtt.sentence->jobNum = m_lastJobNum;
|
|
jobUtt.sentence->seq = utt->sentence->seq;
|
|
jobUtt.audioUrl = TQString();
|
|
jobUtt.utType = utStartOfJob;
|
|
jobUtt.isSSML = false;
|
|
jobUtt.plugin = 0;
|
|
jobUtt.state = usWaitingSignal;
|
|
m_uttQueue.append(jobUtt);
|
|
}
|
|
}
|
|
m_lastSeq = utt->sentence->seq;
|
|
// Add the new utterance to the queue.
|
|
m_uttQueue.append(*utt);
|
|
}
|
|
delete utt;
|
|
} else {
|
|
// If no more text to speak, and we finished the last job, issue textFinished signal.
|
|
if (m_lastJobNum)
|
|
{
|
|
if (m_lastSeq == static_cast<uint>(m_speechData->getTextCount(m_lastJobNum)))
|
|
{
|
|
Utt jobUtt;
|
|
jobUtt.sentence = new mlText;
|
|
jobUtt.sentence->text = TQString();
|
|
jobUtt.sentence->talker = TQString();
|
|
jobUtt.sentence->appId = m_lastAppId;
|
|
jobUtt.sentence->jobNum = m_lastJobNum;
|
|
jobUtt.sentence->seq = 0;
|
|
jobUtt.audioUrl = TQString();
|
|
jobUtt.utType = utEndOfJob;
|
|
jobUtt.isSSML = false;
|
|
jobUtt.plugin = 0;
|
|
jobUtt.state = usWaitingSignal;
|
|
m_uttQueue.append(jobUtt);
|
|
gotOne = true;
|
|
++m_lastSeq; // Don't append another End-of-job
|
|
}
|
|
}
|
|
}
|
|
|
|
return gotOne;
|
|
}
|
|
|
|
/**
|
|
* Given an iterator pointing to the m_uttQueue, deletes the utterance
|
|
* from the queue. If the utterance is currently being processed by a
|
|
* plugin or the Audio Player, halts that operation and deletes Audio Player.
|
|
* Also takes care of deleting temporary audio file.
|
|
* @param it Iterator pointer to m_uttQueue.
|
|
* @return Iterator pointing to the next utterance in the
|
|
* queue, or m_uttQueue.end().
|
|
*/
|
|
uttIterator Speaker::deleteUtterance(uttIterator it)
|
|
{
|
|
switch (it->state)
|
|
{
|
|
case usNone:
|
|
case usWaitingTransform:
|
|
case usWaitingSay:
|
|
case usWaitingSynth:
|
|
case usWaitingSignal:
|
|
case usSynthed:
|
|
case usFinished:
|
|
case usStretched:
|
|
break;
|
|
|
|
case usTransforming:
|
|
{
|
|
delete it->transformer;
|
|
it->transformer = 0;
|
|
break;
|
|
}
|
|
case usSaying:
|
|
case usSynthing:
|
|
{
|
|
// If plugin supports asynchronous mode, and it is busy, halt it.
|
|
PlugInProc* plugin = it->plugin;
|
|
if (it->plugin->supportsAsync())
|
|
if ((plugin->getState() == psSaying) || (plugin->getState() == psSynthing))
|
|
{
|
|
kdDebug() << "Speaker::deleteUtterance calling stopText" << endl;
|
|
plugin->stopText();
|
|
}
|
|
break;
|
|
}
|
|
case usStretching:
|
|
{
|
|
delete it->audioStretcher;
|
|
it->audioStretcher = 0;
|
|
break;
|
|
}
|
|
case usPlaying:
|
|
{
|
|
m_timer->stop();
|
|
it->audioPlayer->stop();
|
|
delete it->audioPlayer;
|
|
break;
|
|
}
|
|
case usPaused:
|
|
case usPreempted:
|
|
{
|
|
// Note: Must call stop(), even if player not currently playing. Why?
|
|
it->audioPlayer->stop();
|
|
delete it->audioPlayer;
|
|
break;
|
|
}
|
|
}
|
|
if (!it->audioUrl.isNull())
|
|
{
|
|
// If the audio file was generated by a plugin, delete it.
|
|
if (it->plugin)
|
|
{
|
|
if (m_speechData->keepAudio)
|
|
{
|
|
TQCString seqStr;
|
|
seqStr.sprintf("%08i", it->sentence->seq); // Zero-fill to 8 chars.
|
|
TQCString jobStr;
|
|
jobStr.sprintf("%08i", it->sentence->jobNum);
|
|
TQString dest = m_speechData->keepAudioPath + "/kttsd-" +
|
|
TQString("%1-%2").arg(jobStr.data()).arg(seqStr.data()) + ".wav";
|
|
TQFile::remove(dest);
|
|
TQDir d;
|
|
d.rename(it->audioUrl, dest);
|
|
// TODO: This is always producing the following. Why and how to fix?
|
|
// It moves the files just fine.
|
|
// tdeio (TDEIOJob): stat file:///home/kde-devel/.trinity/share/apps/kttsd/audio/kttsd-5-1.wav
|
|
// tdeio (TDEIOJob): error 11 /home/kde-devel/.trinity/share/apps/kttsd/audio/kttsd-5-1.wav
|
|
// tdeio (TDEIOJob): This seems to be a suitable case for trying to rename before stat+[list+]copy+del
|
|
// TDEIO::move(it->audioUrl, dest, false);
|
|
}
|
|
else
|
|
TQFile::remove(it->audioUrl);
|
|
}
|
|
}
|
|
// Delete the utterance from queue.
|
|
delete it->sentence;
|
|
return m_uttQueue.erase(it);
|
|
}
|
|
|
|
/**
|
|
* Given an iterator pointing to the m_uttQueue, starts playing audio if
|
|
* 1) An audio file is ready to be played, and
|
|
* 2) It is not already playing.
|
|
* If another audio player is already playing, pauses it before starting
|
|
* the new audio player.
|
|
* @param it Iterator pointer to m_uttQueue.
|
|
* @return True if an utterance began playing or resumed.
|
|
*/
|
|
bool Speaker::startPlayingUtterance(uttIterator it)
|
|
{
|
|
// kdDebug() << "Speaker::startPlayingUtterance running" << endl;
|
|
if (it->state == usPlaying) return false;
|
|
if (it->audioUrl.isNull()) return false;
|
|
bool started = false;
|
|
// Pause (preempt) any other utterance currently being spoken.
|
|
// If any plugins are audibilizing, must wait for them to finish.
|
|
uttIterator itEnd = m_uttQueue.end();
|
|
for (uttIterator it2 = m_uttQueue.begin(); it2 != itEnd; ++it2)
|
|
if (it2 != it)
|
|
{
|
|
if (it2->state == usPlaying)
|
|
{
|
|
m_timer->stop();
|
|
it2->audioPlayer->pause();
|
|
it2->state = usPreempted;
|
|
}
|
|
if (it2->state == usSaying) return false;
|
|
}
|
|
uttState utState = it->state;
|
|
switch (utState)
|
|
{
|
|
case usNone:
|
|
case usWaitingTransform:
|
|
case usTransforming:
|
|
case usWaitingSay:
|
|
case usWaitingSynth:
|
|
case usWaitingSignal:
|
|
case usSaying:
|
|
case usSynthing:
|
|
case usSynthed:
|
|
case usStretching:
|
|
case usPlaying:
|
|
case usFinished:
|
|
break;
|
|
|
|
case usStretched:
|
|
{
|
|
// Don't start playback yet if text job is paused.
|
|
if ((it->utType != utText) ||
|
|
(m_speechData->getTextJobState(it->sentence->jobNum) != KSpeech::jsPaused))
|
|
{
|
|
|
|
it->audioPlayer = createPlayerObject();
|
|
if (it->audioPlayer)
|
|
{
|
|
it->audioPlayer->startPlay(it->audioUrl);
|
|
// Set job to speaking state and set sequence number.
|
|
mlText* sentence = it->sentence;
|
|
m_currentJobNum = sentence->jobNum;
|
|
m_speechData->setTextJobState(m_currentJobNum, KSpeech::jsSpeaking);
|
|
m_speechData->setJobSequenceNum(m_currentJobNum, sentence->seq);
|
|
prePlaySignals(it);
|
|
it->state = usPlaying;
|
|
if (!m_timer->start(timerInterval, FALSE))
|
|
kdDebug() << "Speaker::startPlayingUtterance: timer.start failed" << endl;
|
|
started = true;
|
|
} else {
|
|
// If could not create audio player object, best we can do is silence.
|
|
it->state = usFinished;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case usPaused:
|
|
{
|
|
// Unpause playback only if user has resumed.
|
|
// kdDebug() << "Speaker::startPlayingUtterance: checking whether to resume play" << endl;
|
|
if ((it->utType != utText) ||
|
|
(m_speechData->getTextJobState(it->sentence->jobNum) != KSpeech::jsPaused))
|
|
{
|
|
// kdDebug() << "Speaker::startPlayingUtterance: resuming play" << endl;
|
|
it->audioPlayer->startPlay(TQString()); // resume
|
|
it->state = usPlaying;
|
|
if (!m_timer->start(timerInterval, FALSE))
|
|
kdDebug() << "Speaker::startPlayingUtterance: timer.start failed" << endl;
|
|
started = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case usPreempted:
|
|
{
|
|
// Preempted playback automatically resumes.
|
|
// Note: Must call stop(), even if player not currently playing. Why?
|
|
it->audioPlayer->startPlay(TQString()); // resume
|
|
it->state = usPlaying;
|
|
if (!m_timer->start(timerInterval, FALSE))
|
|
kdDebug() << "Speaker::startPlayingUtterance: timer.start failed" << endl;
|
|
started = true;
|
|
break;
|
|
}
|
|
}
|
|
return started;
|
|
}
|
|
|
|
/**
|
|
* Takes care of emitting reading interrupted/resumed and sentence started signals.
|
|
* Should be called just before audibilizing an utterance.
|
|
* @param it Iterator pointer to m_uttQueue.
|
|
* It also makes sure the job state is set to jsSpeaking.
|
|
*/
|
|
void Speaker::prePlaySignals(uttIterator it)
|
|
{
|
|
uttType utType = it->utType;
|
|
if (utType == utText)
|
|
{
|
|
// If this utterance is for a regular text message,
|
|
// and it was interrupted, emit reading resumed signal.
|
|
if (m_textInterrupted)
|
|
{
|
|
m_textInterrupted = false;
|
|
emit readingResumed();
|
|
}
|
|
// Set job to speaking state and set sequence number.
|
|
mlText* sentence = it->sentence;
|
|
// Emit sentence started signal.
|
|
emit sentenceStarted(sentence->text,
|
|
sentence->talker, sentence->appId,
|
|
m_currentJobNum, sentence->seq);
|
|
} else {
|
|
// If this utterance is not a regular text message,
|
|
// and we are doing a text job, emit reading interrupted signal.
|
|
if (isSpeakingText())
|
|
{
|
|
m_textInterrupted = true;
|
|
emit readingInterrupted();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Takes care of emitting sentenceFinished signal.
|
|
* Should be called immediately after an utterance has completed playback.
|
|
* @param it Iterator pointer to m_uttQueue.
|
|
*/
|
|
void Speaker::postPlaySignals(uttIterator it)
|
|
{
|
|
uttType utType = it->utType;
|
|
if (utType == utText)
|
|
{
|
|
// If this utterance is for a regular text message,
|
|
// emit sentence finished signal.
|
|
mlText* sentence = it->sentence;
|
|
emit sentenceFinished(sentence->appId,
|
|
sentence->jobNum, sentence->seq);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Constructs a temporary filename for plugins to use as a suggested filename
|
|
* for synthesis to write to.
|
|
* @return Full pathname of suggested file.
|
|
*/
|
|
TQString Speaker::makeSuggestedFilename()
|
|
{
|
|
KTempFile tempFile (locateLocal("tmp", "kttsd-"), ".wav");
|
|
TQString waveFile = tempFile.file()->name();
|
|
tempFile.close();
|
|
TQFile::remove(waveFile);
|
|
// kdDebug() << "Speaker::makeSuggestedFilename: Suggesting filename: " << waveFile << endl;
|
|
return TDEStandardDirs::realFilePath(waveFile);
|
|
}
|
|
|
|
/**
|
|
* Creates and returns a player object based on user option.
|
|
*/
|
|
Player* Speaker::createPlayerObject()
|
|
{
|
|
Player* player = 0;
|
|
TQString plugInName;
|
|
switch(m_playerOption)
|
|
{
|
|
case 1 :
|
|
{
|
|
plugInName = "kttsd_gstplugin";
|
|
break;
|
|
}
|
|
case 2 :
|
|
{
|
|
plugInName = "kttsd_alsaplugin";
|
|
break;
|
|
}
|
|
case 3 :
|
|
{
|
|
plugInName = "kttsd_akodeplugin";
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
plugInName = "kttsd_artsplugin";
|
|
break;
|
|
}
|
|
}
|
|
TDETrader::OfferList offers = TDETrader::self()->query(
|
|
"KTTSD/AudioPlugin", TQString("DesktopEntryName == '%1'").arg(plugInName));
|
|
|
|
if(offers.count() == 1)
|
|
{
|
|
kdDebug() << "Speaker::createPlayerObject: Loading " << offers[0]->library() << endl;
|
|
KLibFactory *factory = KLibLoader::self()->factory(offers[0]->library().latin1());
|
|
if (factory)
|
|
player =
|
|
KParts::ComponentFactory::createInstanceFromLibrary<Player>(
|
|
offers[0]->library().latin1(), this, offers[0]->library().latin1());
|
|
}
|
|
if (player == 0)
|
|
{
|
|
// If we tried for GStreamer or ALSA plugin and failed, fall back to aRts plugin.
|
|
if (m_playerOption != 0)
|
|
{
|
|
kdDebug() << "Speaker::createPlayerObject: Could not load " + plugInName +
|
|
" plugin. Falling back to aRts." << endl;
|
|
m_playerOption = 0;
|
|
return createPlayerObject();
|
|
}
|
|
else
|
|
kdDebug() << "Speaker::createPlayerObject: Could not load aRts plugin. Is TDEDIRS set correctly?" << endl;
|
|
} else
|
|
// Must have GStreamer >= 0.8.7. If not, use aRts.
|
|
if (m_playerOption == 1)
|
|
{
|
|
if (!player->requireVersion(0, 8, 7))
|
|
{
|
|
delete player;
|
|
m_playerOption = 0;
|
|
return createPlayerObject();
|
|
}
|
|
}
|
|
if (player) {
|
|
player->setSinkName(m_sinkName);
|
|
player->setPeriodSize(m_periodSize);
|
|
player->setPeriods(m_periodSize);
|
|
player->setDebugLevel(m_playerDebugLevel);
|
|
}
|
|
return player;
|
|
}
|
|
|
|
|
|
/* Slots ==========================================================*/
|
|
|
|
/**
|
|
* Received from PlugIn objects when they finish asynchronous synthesis
|
|
* and audibilizing.
|
|
*/
|
|
void Speaker::slotSayFinished()
|
|
{
|
|
// Since this signal handler may be running from a plugin's thread,
|
|
// convert to postEvent and return immediately.
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 101);
|
|
TQApplication::postEvent(this, ev);
|
|
}
|
|
|
|
/**
|
|
* Received from PlugIn objects when they finish asynchronous synthesis.
|
|
*/
|
|
void Speaker::slotSynthFinished()
|
|
{
|
|
// Since this signal handler may be running from a plugin's thread,
|
|
// convert to postEvent and return immediately.
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 102);
|
|
TQApplication::postEvent(this, ev);
|
|
}
|
|
|
|
/**
|
|
* Received from PlugIn objects when they asynchronously stopText.
|
|
*/
|
|
void Speaker::slotStopped()
|
|
{
|
|
// Since this signal handler may be running from a plugin's thread,
|
|
// convert to postEvent and return immediately.
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 103);
|
|
TQApplication::postEvent(this, ev);
|
|
}
|
|
|
|
/**
|
|
* Received from audio stretcher when stretching (speed adjustment) is finished.
|
|
*/
|
|
void Speaker::slotStretchFinished()
|
|
{
|
|
// Convert to postEvent and return immediately.
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 104);
|
|
TQApplication::postEvent(this, ev);
|
|
}
|
|
|
|
/**
|
|
* Received from transformer (SSMLConvert) when transforming is finished.
|
|
*/
|
|
void Speaker::slotTransformFinished()
|
|
{
|
|
// Convert to postEvent and return immediately.
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 105);
|
|
TQApplication::postEvent(this, ev);
|
|
}
|
|
|
|
/** Received from PlugIn object when they encounter an error.
|
|
* @param keepGoing True if the plugin can continue processing.
|
|
* False if the plugin cannot continue, for example,
|
|
* the speech engine could not be started.
|
|
* @param msg Error message.
|
|
*/
|
|
void Speaker::slotError(bool /*keepGoing*/, const TQString& /*msg*/)
|
|
{
|
|
// Since this signal handler may be running from a plugin's thread,
|
|
// convert to postEvent and return immediately.
|
|
// TODO: Do something with error messages.
|
|
/*
|
|
if (keepGoing)
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 106);
|
|
else
|
|
TQCustomEvent* ev = new TQCustomEvent(TQEvent::User + 107);
|
|
TQApplication::postEvent(this, ev);
|
|
*/
|
|
}
|
|
|
|
/**
|
|
* Received from Timer when it fires.
|
|
* Check audio player to see if it is finished.
|
|
*/
|
|
void Speaker::slotTimeout()
|
|
{
|
|
uttIterator itEnd = m_uttQueue.end();
|
|
for (uttIterator it = m_uttQueue.begin(); it != itEnd; ++it)
|
|
{
|
|
if (it->state == usPlaying)
|
|
{
|
|
if (it->audioPlayer->playing()) return; // Still playing.
|
|
m_timer->stop();
|
|
postPlaySignals(it);
|
|
deleteUtterance(it);
|
|
doUtterances();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Processes events posted by plugins. When asynchronous plugins emit signals
|
|
* they are converted into these events.
|
|
*/
|
|
bool Speaker::event ( TQEvent * e )
|
|
{
|
|
// TODO: Do something with event numbers 106 (error; keepGoing=True)
|
|
// and 107 (error; keepGoing=False).
|
|
if ((e->type() >= (TQEvent::User + 101)) && (e->type() <= (TQEvent::User + 105)))
|
|
{
|
|
// kdDebug() << "Speaker::event: received event." << endl;
|
|
doUtterances();
|
|
return TRUE;
|
|
}
|
|
else return FALSE;
|
|
}
|
|
|