/*
* vxml.cxx
*
* VXML engine for pwlib library
*
* Copyright (C) 2002 Equivalence Pty. Ltd.
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
* the License for the specific language governing rights and limitations
* under the License.
*
* The Original Code is Portable Windows Library.
*
* The Initial Developer of the Original Code is Equivalence Pty. Ltd.
*
* Contributor(s): ______________________________________.
*
* $Log: vxml.cxx,v $
* Revision 1.66 2005/12/01 01:05:59 csoutheren
* Fixed uninitialised variable
*
* Revision 1.65 2005/11/30 12:47:41 csoutheren
* Removed tabs, reformatted some code, and changed tags for Doxygen
*
* Revision 1.64 2005/10/30 23:25:52 csoutheren
* Fixed formatting
* Removed throw() declarations (PWLib does not do exceptions)
* Removed duplicate destructor declarations and definitions
*
* Revision 1.63 2005/10/30 19:41:53 dominance
* fixed most of the warnings occuring during compilation
*
* Revision 1.62 2005/10/21 08:18:21 csoutheren
* Fixed Close operator
*
* Revision 1.61 2005/08/13 06:38:22 rjongbloed
* Fixed illegal code line, assigning to const object!
*
* Revision 1.60 2005/08/12 16:41:51 shorne
* A couple more small fixes thx. Nickolay V. Shmyrev
*
* Revision 1.59 2005/08/11 08:48:10 shorne
* Removed Close from PVXMLSession::Open method. Thanks Nickolay V. Shmyrev
*
* Revision 1.58 2005/05/12 13:40:45 csoutheren
* Fixed locking problems with currentPLayItem optimisation
*
* Revision 1.57 2005/05/12 05:28:36 csoutheren
* Optimised read loop and fixed problems with playing repeated continuous tones
*
* Revision 1.56 2005/04/21 05:28:58 csoutheren
* Fixed assert if recordable does not queue properly
*
* Revision 1.55 2005/03/19 02:52:54 csoutheren
* Fix warnings from gcc 4.1-20050313 shapshot
*
* Revision 1.54 2004/12/03 02:06:05 csoutheren
* Ensure FlushQueue called OnStop for queued elements
*
* Revision 1.53 2004/08/09 11:10:34 csoutheren
* Changed SetTextToSpeech to return ptr to new engine
*
* Revision 1.52 2004/07/28 02:01:51 csoutheren
* Removed deadlock in some call shutdown scenarios
*
* Revision 1.51 2004/07/27 05:26:46 csoutheren
* Fixed recording
*
* Revision 1.50 2004/07/27 00:00:41 csoutheren
* Allowed Close to set closed flag before attepting lock of channels
*
* Revision 1.49 2004/07/26 07:25:02 csoutheren
* Fixed another problem with thread starvation due to delaying inside a mutex lock
*
* Revision 1.48 2004/07/26 00:40:41 csoutheren
* Fixed thread starvation problem under Linux by splitting channelMutex
* into seperate read and write mutexes
*
* Revision 1.47 2004/07/23 00:59:26 csoutheren
* Check in latest changes
*
* Revision 1.46 2004/07/17 09:44:12 rjongbloed
* Fixed missing set of last write count if not actually writing frames.
*
* Revision 1.45 2004/07/15 03:12:42 csoutheren
* Migrated changes from crs_vxnml_devel branch into main trunk
*
* Revision 1.42.2.7 2004/07/13 08:13:05 csoutheren
* Lots of implementation of factory-based PWAVFile
*
* Revision 1.42.2.6 2004/07/12 08:30:17 csoutheren
* More fixes for abstract factory implementation of PWAVFile
*
* Revision 1.42.2.5 2004/07/08 04:58:11 csoutheren
* Exposed VXML playable classes to allow descendants
*
* Revision 1.42.2.4 2004/07/07 07:07:43 csoutheren
* Changed PWAVFile to use abstract factories (extensively)
* Removed redundant blocking/unblocking when using G.723.1
* More support for call transfer
*
* Revision 1.42.2.3 2004/07/06 01:38:57 csoutheren
* Changed PVXMLChannel to use PDelayChannel
* Fixed bug where played files were deleted after playing
*
* Revision 1.42.2.2 2004/07/02 07:22:40 csoutheren
* Updated for latest factory changes
*
* Revision 1.42.2.1 2004/06/20 11:18:03 csoutheren
* Rewrite of resource cacheing to cache text-to-speech output
*
* Revision 1.42 2004/06/19 07:21:08 csoutheren
* Change TTS engine registration to use abstract factory code
* Started disentanglement of PVXMLChannel from PVXMLSession
* Fixed problem with VXML session closing if played data file is not exact frame size multiple
* Allowed PVXMLSession to be used without a VXML script
* Changed PVXMLChannel to handle "file:" URLs
* Numerous other small improvements and optimisations
*
* Revision 1.41 2004/06/02 08:29:28 csoutheren
* Added new code from Andreas Sikkema to implement various VXML features
*
* Revision 1.40 2004/06/02 06:16:48 csoutheren
* Removed unnecessary buffer copying and removed potential busy loop
*
* Revision 1.39 2004/05/02 05:14:43 rjongbloed
* Fixed possible deadlock in shutdown of VXML channel/session.
*
* Revision 1.38 2004/04/24 06:27:56 rjongbloed
* Fixed GCC 3.4.0 warnings about PAssertNULL and improved recoverability on
* NULL pointer usage in various bits of code.
*
* Revision 1.37 2004/03/23 04:48:42 csoutheren
* Improved ability to start VXML scripts as needed
*
* Revision 1.36 2003/11/12 20:38:16 csoutheren
* Fixed problem with incorrect sense of ContentLength header detection thanks to Andreas Sikkema
*
* Revision 1.35 2003/05/14 01:12:53 rjongbloed
* Fixed test for SID frames in record silence detection on G.723.1A
*
* Revision 1.34 2003/04/23 11:54:53 craigs
* Added ability to record audio
*
* Revision 1.33 2003/04/10 04:19:43 robertj
* Fixed incorrect timing on G.723.1 (framed codec)
* Fixed not using correct codec file suffix for non PCM/G.723.1 codecs.
*
* Revision 1.32 2003/04/08 05:09:14 craigs
* Added ability to use commands as an audio source
*
* Revision 1.31 2003/03/17 08:03:07 robertj
* Combined to the separate incoming and outgoing substream classes into
* a single class to make it easier to produce codec aware descendents.
* Added G.729 substream class.
*
* Revision 1.30 2002/12/03 22:39:14 robertj
* Removed get document that just returns a content length as the chunked
* transfer encoding makes this very dangerous.
*
* Revision 1.29 2002/11/19 10:36:30 robertj
* Added functions to set anf get "file:" URL. as PFilePath and do the right
* things with platform dependent directory components.
*
* Revision 1.28 2002/11/08 03:39:27 craigs
* Fixed problem with G.723.1 files
*
* Revision 1.27 2002/09/24 13:47:41 robertj
* Added support for more vxml commands, thanks Alexander Kovatch
*
* Revision 1.26 2002/09/18 06:37:40 robertj
* Added functions to load vxml directly, via file or URL. Old function
* intelligently picks which one to use.
*
* Revision 1.25 2002/09/03 04:38:14 craigs
* Added VXML 2.0 time attribute to <break>
*
* Revision 1.24 2002/09/03 04:11:37 craigs
* More changes from Alexander Kovatch
*
* Revision 1.23 2002/08/30 07:33:16 craigs
* Added extra initialisations
*
* Revision 1.22 2002/08/30 05:05:54 craigs
* Added changes for PVXMLGrammar from Alexander Kovatch
*
* Revision 1.21 2002/08/29 00:16:12 craigs
* Fixed typo, thanks to Peter Robinson
*
* Revision 1.20 2002/08/28 08:05:16 craigs
* Reorganised VXMLSession class as per code from Alexander Kovatch
*
* Revision 1.19 2002/08/28 05:10:57 craigs
* Added ability to load resources via URI
* Added cache
*
* Revision 1.18 2002/08/27 02:46:56 craigs
* Removed need for application to call AllowClearCall
*
* Revision 1.17 2002/08/27 02:20:09 craigs
* Added <break> command in prompt blocks
* Fixed potential deadlock
* Added <prompt> command in top level fields, thanks to Alexander Kovatch
*
* Revision 1.16 2002/08/15 04:11:16 robertj
* Fixed shutdown problems with closing vxml session, leaks a thread.
* Fixed potential problems with indirect channel Close() function.
*
* Revision 1.15 2002/08/15 02:13:10 craigs
* Fixed problem with handle leak (maybe) and change tts files back to autodelete
*
* Revision 1.14 2002/08/14 15:18:07 craigs
* Improved random filename generation
*
* Revision 1.13 2002/08/08 01:03:06 craigs
* Added function to re-enable automatic call clearing on script end
*
* Revision 1.12 2002/08/07 13:38:14 craigs
* Fixed bug in calculating lengths of G.723.1 packets
*
* Revision 1.11 2002/08/06 07:45:28 craigs
* Added lots of stuff from OpalVXML
*
* Revision 1.10 2002/07/29 15:08:50 craigs
* Added autodelete option to PlayFile
*
* Revision 1.9 2002/07/29 15:03:36 craigs
* Added access to queue functions
* Added autodelete option to AddFile
*
* Revision 1.8 2002/07/29 14:16:05 craigs
* Added asynchronous VXML execution
*
* Revision 1.7 2002/07/17 08:34:25 craigs
* Fixed deadlock problems
*
* Revision 1.6 2002/07/17 06:08:23 craigs
* Added additional "sayas" classes
*
* Revision 1.5 2002/07/10 13:15:20 craigs
* Moved some VXML classes from Opal back into PTCLib
* Fixed various race conditions
*
* Revision 1.4 2002/07/05 06:28:07 craigs
* Added OnEmptyAction callback
*
* Revision 1.3 2002/07/02 06:24:53 craigs
* Added recording functions
*
* Revision 1.2 2002/06/28 01:30:29 robertj
* Fixed ability to compile if do not have expat library.
*
* Revision 1.1 2002/06/27 05:27:49 craigs
* Initial version
*
*
*/
#ifdef __GNUC__
#pragma implementation "vxml.h"
#endif
#include <ptlib.h>
#define P_DISABLE_FACTORY_INSTANCES
#if P_EXPAT
#include <ptlib/pfactory.h>
#include <ptclib/vxml.h>
#include <ptclib/memfile.h>
#include <ptclib/random.h>
#include <ptclib/http.h>
#define SMALL_BREAK_MSECS 1000
#define MEDIUM_BREAK_MSECS 2500
#define LARGE_BREAK_MSECS 5000
// LATER: Lookup what this value should be
#define DEFAULT_TIMEOUT 10000
//////////////////////////////////////////////////////////
static PString GetContentType(const PFilePath & fn)
{
PString type = fn.GetType();
if (type *= ".vxml")
return "text/vxml";
if (type *= ".wav")
return "audio/x-wav";
return PString::Empty();
}
///////////////////////////////////////////////////////////////
BOOL PVXMLPlayable::ReadFrame(PVXMLChannel & channel, void * _buf, PINDEX origLen)
{
BYTE * buf = (BYTE *)_buf;
PINDEX len = origLen;
while (len > 0) {
BOOL stat = channel.ReadFrame(buf, len);
if (stat)
return TRUE;
if ((repeat == 0) || !Rewind(channel.GetBaseReadChannel()))
return FALSE;
PINDEX readLen = channel.GetLastReadCount();
len -= readLen;
buf += readLen;
}
return TRUE;
}
///////////////////////////////////////////////////////////////
BOOL PVXMLPlayableFilename::Open(PVXMLChannel & chan, const PString & _fn, PINDEX _delay, PINDEX _repeat, BOOL _autoDelete)
{
fn = _fn;
arg = _fn;
if (!PFile::Exists(chan.AdjustWavFilename(fn)))
return FALSE;
return PVXMLPlayable::Open(chan, _delay, _repeat, _autoDelete);
}
void PVXMLPlayableFilename::Play(PVXMLChannel & outgoingChannel)
{
PChannel * chan = NULL;
// check the file extension and open a .wav or a raw (.sw or .g723) file
if ((fn.Right(4)).ToLower() == ".wav")
chan = outgoingChannel.CreateWAVFile(fn);
else {
PFile * fileChan = new PFile(fn);
if (fileChan->Open(PFile::ReadOnly))
chan = fileChan;
else {
delete fileChan;
}
}
if (chan == NULL)
PTRACE(3, "PVXML\tCannot open file \"" << fn << "\"");
else {
PTRACE(3, "PVXML\tPlaying file \"" << fn << "\"");
outgoingChannel.SetReadChannel(chan, TRUE);
}
}
void PVXMLPlayableFilename::OnStop()
{
if (autoDelete) {
PTRACE(3, "PVXML\tDeleting file \"" << fn << "\"");
PFile::Remove(fn);
}
}
BOOL PVXMLPlayableFilename::Rewind(PChannel * chan)
{
PFile * file = dynamic_cast<PFile *>(chan);
if (file == NULL)
return FALSE;
return file->SetPosition(0);
}
PFactory<PVXMLPlayable>::Worker<PVXMLPlayableFilename> vxmlPlayableFilenameFactory("File");
///////////////////////////////////////////////////////////////
BOOL PVXMLPlayableFilenameList::Open(PVXMLChannel & chan, const PStringArray & _list, PINDEX _delay, PINDEX _repeat, BOOL _autoDelete)
{
for (PINDEX i = 0; i < _list.GetSize(); ++i) {
PString fn = chan.AdjustWavFilename(_list[i]);
if (PFile::Exists(fn))
filenames.AppendString(fn);
}
if (filenames.GetSize() == 0)
return FALSE;
currentIndex = 0;
return PVXMLPlayable::Open(chan, _delay, ((_repeat >= 0) ? _repeat : 1) * filenames.GetSize(), _autoDelete);
}
void PVXMLPlayableFilenameList::OnRepeat(PVXMLChannel & outgoingChannel)
{
PFilePath fn = filenames[currentIndex++ % filenames.GetSize()];
PChannel * chan = NULL;
// check the file extension and open a .wav or a raw (.sw or .g723) file
if ((fn.Right(4)).ToLower() == ".wav")
chan = outgoingChannel.CreateWAVFile(fn);
else {
PFile * fileChan = new PFile(fn);
if (fileChan->Open(PFile::ReadOnly))
chan = fileChan;
else {
delete fileChan;
}
}
if (chan == NULL)
PTRACE(3, "PVXML\tCannot open file \"" << fn << "\"");
else {
PTRACE(3, "PVXML\tPlaying file \"" << fn << "\"");
outgoingChannel.SetReadChannel(chan, TRUE);
}
}
void PVXMLPlayableFilenameList::OnStop()
{
if (autoDelete) {
for (PINDEX i = 0; i < filenames.GetSize(); ++i) {
PTRACE(3, "PVXML\tDeleting file \"" << filenames[i] << "\"");
PFile::Remove(filenames[i]);
}
}
}
PFactory<PVXMLPlayable>::Worker<PVXMLPlayableFilenameList> vxmlPlayableFilenameListFactory("FileList");
///////////////////////////////////////////////////////////////
PVXMLPlayableCommand::PVXMLPlayableCommand()
{
pipeCmd = NULL;
}
void PVXMLPlayableCommand::Play(PVXMLChannel & outgoingChannel)
{
arg.Replace("%s", PString(PString::Unsigned, sampleFrequency));
arg.Replace("%f", format);
// execute a command and send the output through the stream
pipeCmd = new PPipeChannel;
if (!pipeCmd->Open(arg, PPipeChannel::ReadOnly)) {
PTRACE(3, "PVXML\tCannot open command " << arg);
delete pipeCmd;
return;
}
if (pipeCmd == NULL)
PTRACE(3, "PVXML\tCannot open command \"" << arg << "\"");
else {
pipeCmd->Execute();
PTRACE(3, "PVXML\tPlaying command \"" << arg << "\"");
outgoingChannel.SetReadChannel(pipeCmd, TRUE);
}
}
void PVXMLPlayableCommand::OnStop()
{
if (pipeCmd != NULL) {
pipeCmd->WaitForTermination();
delete pipeCmd;
}
}
PFactory<PVXMLPlayable>::Worker<PVXMLPlayableCommand> vxmlPlayableCommandFactory("Command");
///////////////////////////////////////////////////////////////
BOOL PVXMLPlayableData::Open(PVXMLChannel & chan, const PString & /*_fn*/, PINDEX _delay, PINDEX _repeat, BOOL v)
{
return PVXMLPlayable::Open(chan, _delay, _repeat, v);
}
void PVXMLPlayableData::SetData(const PBYTEArray & _data)
{
data = _data;
}
void PVXMLPlayableData::Play(PVXMLChannel & outgoingChannel)
{
PMemoryFile * chan = new PMemoryFile(data);
PTRACE(3, "PVXML\tPlaying " << data.GetSize() << " bytes");
outgoingChannel.SetReadChannel(chan, TRUE);
}
BOOL PVXMLPlayableData::Rewind(PChannel * chan)
{
PMemoryFile * memfile = dynamic_cast<PMemoryFile *>(chan);
if (memfile == NULL)
return FALSE;
return memfile->SetPosition(0);
}
PFactory<PVXMLPlayable>::Worker<PVXMLPlayableData> vxmlPlayableDataFactory("PCM Data");
///////////////////////////////////////////////////////////////
BOOL PVXMLPlayableURL::Open(PVXMLChannel & chan, const PString & _url, PINDEX _delay, PINDEX _repeat, BOOL autoDelete)
{
url = arg = _url;
return PVXMLPlayable::Open(chan, _delay, _repeat, autoDelete);
}
void PVXMLPlayableURL::Play(PVXMLChannel & outgoingChannel)
{
// open the resource
PHTTPClient * client = new PHTTPClient;
PMIMEInfo outMIME, replyMIME;
int code = client->GetDocument(url, outMIME, replyMIME, FALSE);
if ((code != 200) || (replyMIME(PHTTP::TransferEncodingTag) *= PHTTP::ChunkedTag))
delete client;
else {
outgoingChannel.SetReadChannel(client, TRUE);
}
}
PFactory<PVXMLPlayable>::Worker<PVXMLPlayableURL> vxmlPlayableURLFactory("URL");
///////////////////////////////////////////////////////////////
BOOL PVXMLRecordableFilename::Open(const PString & _arg)
{
fn = _arg;
consecutiveSilence = 0;
return TRUE;
}
void PVXMLRecordableFilename::Record(PVXMLChannel & outgoingChannel)
{
PChannel * chan = NULL;
// check the file extension and open a .wav or a raw (.sw or .g723) file
if ((fn.Right(4)).ToLower() == ".wav")
chan = outgoingChannel.CreateWAVFile(fn, TRUE);
else {
PFile * fileChan = new PFile(fn);
if (fileChan->Open(PFile::WriteOnly))
chan = fileChan;
else {
delete fileChan;
}
}
if (chan == NULL)
PTRACE(3, "PVXML\tCannot open file \"" << fn << "\"");
else {
PTRACE(3, "PVXML\tRecording to file \"" << fn << "\"");
outgoingChannel.SetWriteChannel(chan, TRUE);
}
recordStart = PTime();
silenceStart = PTime();
consecutiveSilence = 0;
}
BOOL PVXMLRecordableFilename::OnFrame(BOOL isSilence)
{
if (!isSilence) {
silenceStart = PTime();
consecutiveSilence = 0;
} else {
consecutiveSilence++;
if ( ((consecutiveSilence % 20) == 0) &&
(
((finalSilence > 0) && ((PTime() - silenceStart).GetMilliSeconds() >= finalSilence)) ||
((maxDuration > 0) && ((PTime() - recordStart).GetMilliSeconds() >= maxDuration))
)
)
return TRUE;
}
return FALSE;
}
///////////////////////////////////////////////////////////////
PVXMLCache::PVXMLCache(const PDirectory & _directory)
: directory(_directory)
{
if (!directory.Exists())
directory.Create();
}
static PString MD5AsHex(const PString & str)
{
PMessageDigest::Result digest;
PMessageDigest5::Encode(str, digest);
PString hexStr;
const BYTE * data = digest.GetPointer();
for (PINDEX i = 0; i < digest.GetSize(); ++i)
hexStr.sprintf("%02x", (unsigned)data[i]);
return hexStr;
}
PFilePath PVXMLCache::CreateFilename(const PString & prefix, const PString & key, const PString & fileType)
{
PString md5 = MD5AsHex(key);
PString md5_2 = MD5AsHex(key);
return directory + ((prefix + "_") + md5 + fileType);
}
BOOL PVXMLCache::Get(const PString & prefix,
const PString & key,
const PString & fileType,
PString & contentType,
PFilePath & dataFn)
{
PWaitAndSignal m(*this);
dataFn = CreateFilename(prefix, key, "." + fileType);
PFilePath typeFn = CreateFilename(prefix, key, "_type.txt");
if (!PFile::Exists(dataFn) || !PFile::Exists(typeFn)) {
PTRACE(4, "Key \"" << key << "\" not found in cache");
return FALSE;
}
PTextFile typeFile(typeFn, PFile::ReadOnly);
if (!typeFile.IsOpen()) {
PTRACE(4, "Cannot find type for cached key " << key << " in cache");
PFile::Remove(dataFn);
return FALSE;
}
typeFile.ReadLine(contentType);
contentType.Trim();
if (contentType.IsEmpty())
contentType = GetContentType(dataFn);
return TRUE;
}
void PVXMLCache::Put(const PString & prefix,
const PString & key,
const PString & fileType,
const PString & contentType,
const PFilePath & fn,
PFilePath & dataFn)
{
PWaitAndSignal m(*this);
// create the filename for the cache files
dataFn = CreateFilename(prefix, key, "." + fileType);
PFilePath typeFn = CreateFilename(prefix, key, "_type.txt");
// write the content type file
PTextFile typeFile(typeFn, PFile::WriteOnly);
if (contentType.IsEmpty())
typeFile.WriteLine(GetContentType(fn));
else
typeFile.WriteLine(contentType);
// rename the file to the correct name
PFile::Rename(fn, dataFn.GetFileName(), TRUE);
}
PVXMLCache & PVXMLCache::GetResourceCache()
{
static PVXMLCache cache(PDirectory() + "cache");
return cache;
}
PFilePath PVXMLCache::GetRandomFilename(const PString & prefix, const PString & fileType)
{
PFilePath fn;
// create a random temporary filename
PRandom r;
for (;;) {
fn = directory + psprintf("%s_%i.%s", (const char *)prefix, r.Generate() % 1000000, (const char *)fileType);
if (!PFile::Exists(fn))
break;
}
return fn;
}
//////////////////////////////////////////////////////////
PVXMLSession::PVXMLSession(PTextToSpeech * _tts, BOOL autoDelete)
{
vxmlThread = NULL;
vxmlChannel = NULL;
finishWhenEmpty = TRUE;
textToSpeech = NULL;
SetTextToSpeech(_tts, autoDelete);
Initialise();
}
void PVXMLSession::Initialise()
{
recording = FALSE;
allowFinish = FALSE;
listening = FALSE;
activeGrammar = NULL;
listening = FALSE;
forceEnd = FALSE;
currentForm = NULL;
currentNode = NULL;
autoDeleteTextToSpeech = FALSE;
}
PVXMLSession::~PVXMLSession()
{
Close();
if ((textToSpeech != NULL) && autoDeleteTextToSpeech)
delete textToSpeech;
}
PTextToSpeech * PVXMLSession::SetTextToSpeech(PTextToSpeech * _tts, BOOL autoDelete)
{
PWaitAndSignal m(sessionMutex);
if (autoDeleteTextToSpeech && (textToSpeech != NULL))
delete textToSpeech;
autoDeleteTextToSpeech = autoDelete;
textToSpeech = _tts;
return textToSpeech;
}
PTextToSpeech * PVXMLSession::SetTextToSpeech(const PString & ttsName)
{
PWaitAndSignal m(sessionMutex);
if (autoDeleteTextToSpeech && (textToSpeech != NULL))
delete textToSpeech;
autoDeleteTextToSpeech = TRUE;
textToSpeech = PFactory<PTextToSpeech>::CreateInstance(ttsName);
return textToSpeech;
}
BOOL PVXMLSession::Load(const PString & source)
{
// Lets try and guess what was passed, if file exists then is file
PFilePath file = source;
if (PFile::Exists(file))
return LoadFile(file);
// see if looks like URL
PINDEX pos = source.Find(':');
if (pos != P_MAX_INDEX) {
PString scheme = source.Left(pos);
if ((scheme *= "http") || (scheme *= "https") || (scheme *= "file"))
return LoadURL(source);
}
// See if is actual VXML
if (PCaselessString(source).Find("<vxml") != P_MAX_INDEX)
return LoadVXML(source);
return FALSE;
}
BOOL PVXMLSession::LoadFile(const PFilePath & filename)
{
// create a file URL from the filename
return LoadURL(filename);
}
BOOL PVXMLSession::LoadURL(const PURL & url)
{
// retreive the document (may be a HTTP get)
PFilePath fn;
PString contentType;
if (!RetreiveResource(url, contentType, fn, FALSE)) {
PTRACE(1, "PVXML\tCannot load document " << url);
return FALSE;
}
PTextFile file(fn, PFile::ReadOnly);
if (!file.IsOpen()) {
PTRACE(1, "PVXML\tCannot read data from " << fn);
return FALSE;
}
off_t len = file.GetLength();
PString text;
file.Read(text.GetPointer(len+1), len);
len = file.GetLastReadCount();
text.SetSize(len+1);
text[(PINDEX)len] = '\0';
if (!LoadVXML(text)) {
PTRACE(1, "PVXML\tCannot load VXML in " << url);
return FALSE;
}
rootURL = url;
return TRUE;
}
BOOL PVXMLSession::LoadVXML(const PString & xmlText)
{
PWaitAndSignal m(sessionMutex);
allowFinish = loaded = FALSE;
rootURL = PString::Empty();
// parse the XML
xmlFile.RemoveAll();
if (!xmlFile.Load(xmlText)) {
PTRACE(1, "PVXML\tCannot parse root document: " << GetXMLError());
return FALSE;
}
PXMLElement * root = xmlFile.GetRootElement();
if (root == NULL)
return FALSE;
// reset interpeter state
Initialise();
// find the first form
if ((currentForm = FindForm(PString::Empty())) == NULL)
return FALSE;
// start processing with this <form> element
currentNode = currentForm;
loaded = TRUE;
return TRUE;
}
PURL PVXMLSession::NormaliseResourceName(const PString & src)
{
// if resource name has a scheme, then use as is
PINDEX pos = src.Find(':');
if ((pos != P_MAX_INDEX) && (pos < 5))
return src;
if (rootURL.IsEmpty())
return "file:" + src;
// else use scheme and path from root document
PURL url = rootURL;
PStringArray path = url.GetPath();
PString pathStr;
if (path.GetSize() > 0) {
pathStr += path[0];
PINDEX i;
for (i = 1; i < path.GetSize()-1; i++)
pathStr += "/" + path[i];
pathStr += "/" + src;
url.SetPathStr(pathStr);
}
return url;
}
BOOL PVXMLSession::RetreiveResource(const PURL & url,
PString & contentType,
PFilePath & dataFn,
BOOL useCache)
{
BOOL stat = FALSE;
// files on the local file system get loaded locally
if (url.GetScheme() *= "file") {
dataFn = url.AsFilePath();
if (contentType.IsEmpty())
contentType = GetContentType(dataFn);
stat = TRUE;
}
// do a HTTP get when appropriate
else if ((url.GetScheme() *= "http") || (url.GetScheme() *= "https")) {
PFilePath fn;
PString fileType = url.AsFilePath().GetType();
BOOL inCache = FALSE;
if (useCache)
inCache = PVXMLCache::GetResourceCache().Get("url", url.AsString(), fileType, contentType, dataFn);
if (!inCache) {
// get a random filename
fn = PVXMLCache::GetResourceCache().GetRandomFilename("url", fileType);
// get the resource header information
PHTTPClient client;
PMIMEInfo outMIME, replyMIME;
if (!client.GetDocument(url, outMIME, replyMIME)) {
PTRACE(2, "PVXML\tCannot load resource " << url);
stat =FALSE;
}
else {
// Get the body of the response in a PBYTEArray (might be binary data)
PBYTEArray incomingData;
client.ReadContentBody(replyMIME, incomingData);
contentType = replyMIME(PHTTPClient::ContentTypeTag);
// write the data in the file
PFile cacheFile(fn, PFile::WriteOnly);
cacheFile.Write(incomingData.GetPointer(), incomingData.GetSize() );
// if we have a cache and we are using it, then save the data
if (useCache)
PVXMLCache::GetResourceCache().Put("url", url.AsString(), fileType, contentType, fn, dataFn);
// data is loaded
stat = TRUE;
}
}
}
// files on the local file system get loaded locally
else if (url.GetScheme() *= "file") {
dataFn = url.AsFilePath();
stat = TRUE;
}
// unknown schemes give an error
else
stat = FALSE;
return stat;
}
PXMLElement * PVXMLSession::FindForm(const PString & id)
{
// NOTE: should have some flag to know if it is loaded
PXMLElement * root = xmlFile.GetRootElement();
if (root == NULL)
return NULL;
// Only handle search of top level nodes for <form> element
PINDEX i;
for (i = 0; i < root->GetSize(); i++) {
PXMLObject * xmlObject = root->GetElement(i);
if (xmlObject->IsElement()) {
PXMLElement * xmlElement = (PXMLElement*)xmlObject;
if (
(xmlElement->GetName() *= "form") &&
(id.IsEmpty() || (xmlElement->GetAttribute("id") *= id))
)
return xmlElement;
}
}
return NULL;
}
BOOL PVXMLSession::Open(BOOL isPCM)
{
if (isPCM)
return Open(VXML_PCM16);
else
return Open(VXML_G7231);
}
BOOL PVXMLSession::Open(const PString & _mediaFormat)
{
mediaFormat = _mediaFormat;
PVXMLChannel * chan = PFactory<PVXMLChannel>::CreateInstance(mediaFormat);
if (chan == NULL) {
PTRACE(1, "VXML\tCannot create VXML channel with format " << mediaFormat);
return FALSE;
}
// set the underlying channel
if (!PIndirectChannel::Open(chan, chan))
return FALSE;
// start the VXML session in another thread
{
PWaitAndSignal m(sessionMutex);
if (!chan->Open(this))
return FALSE;
vxmlChannel = chan;
}
return Execute();
}
BOOL PVXMLSession::Execute()
{
PWaitAndSignal m(sessionMutex);
// cannot open if no data is loaded
if (loaded && vxmlThread == NULL) {
threadRunning = TRUE;
vxmlThread = PThread::Create(PCREATE_NOTIFIER(VXMLExecute), 0, PThread::NoAutoDeleteThread);
}
return TRUE;
}
BOOL PVXMLSession::Close()
{
{
PWaitAndSignal m(sessionMutex);
if (vxmlThread != NULL) {
// Stop condition for thread
threadRunning = FALSE;
forceEnd = TRUE;
waitForEvent.Signal();
// Signal all syncpoints that could be waiting for things
transferSync.Signal();
answerSync.Signal();
vxmlChannel->Close();
vxmlThread->WaitForTermination();
delete vxmlThread;
vxmlThread = NULL;
}
vxmlChannel = NULL;
}
return PIndirectChannel::Close();
}
void PVXMLSession::VXMLExecute(PThread &, INT)
{
while (!forceEnd && threadRunning) {
// process current node in the VXML script
ExecuteDialog();
// wait for something to happen
if (currentNode == NULL || IsPlaying())
waitForEvent.Wait();
}
// Make sure the script has been run to the end so
// submit actions etc. can be performed
// record and audio and other user interaction commands should be skipped
if (forceEnd) {
PTRACE(1, "Fast forwarding through script because of forceEnd" );
while (currentNode != NULL)
ExecuteDialog();
}
OnEndSession();
//PWaitAndSignal m(sessionMutex);
if (vxmlChannel != NULL)
vxmlChannel->Close();
return;
}
void PVXMLSession::ProcessUserInput()
{
// without this initialisation, gcc 4.1 gives a warning
char ch = 0;
{
PWaitAndSignal m(userInputMutex);
if (userInputQueue.size() == 0)
return;
ch = userInputQueue.front();
userInputQueue.pop();
}
PTRACE(3, "VXML\tHandling user input " << ch);
// recording
if (recording) {
if (recordDTMFTerm)
RecordEnd();
}
// playback
else {
if (activeGrammar != NULL)
activeGrammar->OnUserInput(ch);
}
}
void PVXMLSession::ExecuteDialog()
{
// check for user input
ProcessUserInput();
// process any active grammars
ProcessGrammar();
// process current node in the VXML script
ProcessNode();
// Wait for the buffer to complete before continuing to the next node
if (currentNode == NULL) {
if (IsPlaying())
return;
}
// if the current node has children, then process the first child
else if (currentNode->IsElement() && (((PXMLElement *)currentNode)->GetElement(0) != NULL))
currentNode = ((PXMLElement *)currentNode)->GetElement(0);
// else process the next sibling
else {
// Keep moving up the parents until we find a next sibling
while ((currentNode != NULL) && currentNode->GetNextObject() == NULL) {
currentNode = currentNode->GetParent();
// if we are on the backwards traversal through a <field> then wait
// for a grammar recognition and throw events if necessary
if (currentNode != NULL && (currentNode->IsElement() == TRUE) && (((PXMLElement*)currentNode)->GetName() *= "field")) {
listening = TRUE;
PlaySilence(timeout);
}
}
if (currentNode != NULL)
currentNode = currentNode->GetNextObject();
}
// Determine if we should quit
if ((currentNode == NULL) && (activeGrammar == NULL) && !IsPlaying() && !IsRecording() && allowFinish && finishWhenEmpty) {
threadRunning = FALSE;
waitForEvent.Signal();
}
}
void PVXMLSession::ProcessGrammar()
{
if (activeGrammar == NULL)
return;
BOOL processGrammar(FALSE);
// Stop if we've matched a grammar or have a failed recognition
if (activeGrammar->GetState() == PVXMLGrammar::FILLED || activeGrammar->GetState() == PVXMLGrammar::NOMATCH)
processGrammar = TRUE;
// Stop the grammar if we've timed out
else if (listening && !IsPlaying()) {
activeGrammar->Stop();
processGrammar = TRUE;
}
// Let the loop run again if we're still waiting to time out and haven't resolved the grammar one way or the other
if (!processGrammar && listening)
return;
if (processGrammar)
{
PVXMLGrammar::GrammarState state = activeGrammar->GetState();
grammarResult = activeGrammar->GetValue();
LoadGrammar(NULL);
listening = FALSE;
// Stop any playback
if (vxmlChannel != NULL) {
vxmlChannel->FlushQueue();
vxmlChannel->EndRecording();
}
// Check we're not in a menu
if (eventName.IsEmpty()) {
// Figure out what happened
switch (state)
{
case PVXMLGrammar::FILLED:
eventName = "filled";
break;
case PVXMLGrammar::NOINPUT:
eventName = "noinput";
break;
case PVXMLGrammar::NOMATCH:
eventName = "nomatch";
break;
default:
; //ERROR - unexpected grammar state
}
// Find the handler and move there
PXMLElement * handler = FindHandler(eventName);
if (handler != NULL)
currentNode = handler;
}
}
}
void PVXMLSession::ProcessNode()
{
if (currentNode == NULL)
return;
if (!currentNode->IsElement()) {
if (!forceEnd)
TraverseAudio();
else
currentNode = NULL;
}
else {
PXMLElement * element = (PXMLElement*)currentNode;
PCaselessString nodeType = element->GetName();
PTRACE(3, "PVXML\t**** Processing VoiceXML element: <" << nodeType << "> ***");
if (nodeType *= "audio") {
if (!forceEnd)
TraverseAudio();
}
else if (nodeType *= "block") {
// check 'cond' attribute to see if this element's children are to be skipped
// go on and process the children
}
else if (nodeType *= "break")
TraverseAudio();
else if (nodeType *= "disconnect")
currentNode = NULL;
else if (nodeType *= "field") {
currentField = (PXMLElement*)currentNode;
timeout = DEFAULT_TIMEOUT;
TraverseGrammar(); // this will set activeGrammar
}
else if (nodeType *= "form") {
// this is now the current element - go on
currentForm = element;
currentField = NULL; // no active field in a new form
}
else if (nodeType *= "goto")
TraverseGoto();
else if (nodeType *= "grammar")
TraverseGrammar(); // this will set activeGrammar
else if (nodeType *= "record") {
if (!forceEnd)
TraverseRecord();
}
else if (nodeType *= "prompt") {
if (!forceEnd) {
// LATER:
// check 'cond' attribute to see if the children of this node should be processed
// check 'count' attribute to see if this node should be processed
// flush all prompts if 'bargein' attribute is set to false
// Update timeout of current recognition (if 'timeout' attribute is set)
if (element->HasAttribute("timeout")) {
PTimeInterval timeout = StringToTime(element->GetAttribute("timeout"));
}
}
}
else if (nodeType *= "say-as") {
if (!forceEnd) {
}
}
else if (nodeType *= "value") {
if (!forceEnd)
TraverseAudio();
}
else if (nodeType *= "var")
TraverseVar();
else if (nodeType *= "if")
TraverseIf();
else if (nodeType *= "exit")
TraverseExit();
else if (nodeType *= "menu") {
if (!forceEnd) {
TraverseMenu();
eventName = "menu";
}
}
else if (nodeType *= "choice") {
if (!TraverseChoice(grammarResult))
defaultDTMF++;
else {
// If the correct choice has been found,
/// make sure everything is reset correctly
eventName.MakeEmpty();
grammarResult.MakeEmpty();
defaultDTMF = 1;
}
}
else if (nodeType *= "transfer") {
if (!forceEnd)
TraverseTransfer();
}
else if (nodeType *= "submit")
TraverseSubmit();
else if (nodeType *= "property")
TraverseProperty();
}
}
BOOL PVXMLSession::OnUserInput(const PString & str)
{
{
PWaitAndSignal m(userInputMutex);
for (PINDEX i = 0; i < str.GetLength(); i++)
userInputQueue.push(str[i]);
}
waitForEvent.Signal();
return TRUE;
}
BOOL PVXMLSession::TraverseRecord()
{
if (currentNode->IsElement()) {
PString strName;
PXMLElement * element = (PXMLElement *)currentNode;
// Get the name (name)
if (element->HasAttribute("name"))
strName = element->GetAttribute("name");
else if (element->HasAttribute("id"))
strName = element->GetAttribute("id");
// Get the destination filename (dest)
PString strDest;
if (element->HasAttribute("dest"))
strDest = element->GetAttribute("dest");
// see if we need a beep
if (element->GetAttribute("beep").ToLower() *= "true") {
PBYTEArray beepData;
GetBeepData(beepData, 1000);
if (beepData.GetSize() != 0)
PlayData(beepData);
}
if (strDest.IsEmpty()) {
PTime now;
strDest = GetVar("session.telephone.dnis" ) + "_" + GetVar( "session.telephone.ani" ) + "_" + now.AsString( "yyyyMMdd_hhmmss") + ".wav";
}
// For some reason, if the file is there the create
// seems to fail.
PFile::Remove(strDest);
PFilePath file(strDest);
// Get max record time (maxtime)
PTimeInterval maxTime = PMaxTimeInterval;
if (element->HasAttribute("maxtime"))
maxTime = StringToTime(element->GetAttribute("maxtime"));
// Get terminating silence duration (finalsilence)
PTimeInterval termTime(3000);
if (element->HasAttribute("finalsilence"))
termTime = StringToTime(element->GetAttribute("finalsilence"));
// Get dtmf term (dtmfterm)
BOOL dtmfTerm = TRUE;
if (element->HasAttribute("dtmfterm"))
dtmfTerm = !(element->GetAttribute("dtmfterm").ToLower() *= "false");
// create a semaphore, and then wait for the recording to terminate
StartRecording(file, dtmfTerm, maxTime, termTime);
recordSync.Wait(maxTime);
if (!recordSync.Wait(maxTime)) {
// The Wait() has timed out, to signal that the record timed out.
// This is VXML version 2 property, but nice.
// So it's possible to detect if the record timed out from within the
// VXML script
SetVar(strName + "$.maxtime", "true");
}
else {
// Normal hangup before timeout
SetVar( strName + "$.maxtime", "false");
}
// when this returns, we are done
EndRecording();
}
return TRUE;
}
PString PVXMLSession::GetXMLError() const
{
return psprintf("(%i:%i) ", xmlFile.GetErrorLine(), xmlFile.GetErrorColumn()) + xmlFile.GetErrorString();
}
PString PVXMLSession::EvaluateExpr(const PString & oexpr)
{
PString expr = oexpr.Trim();
// see if all digits
PINDEX i;
BOOL allDigits = TRUE;
for (i = 0; i < expr.GetLength(); i++) {
allDigits = allDigits && isdigit(expr[i]);
}
if (allDigits)
return expr;
return GetVar(expr);
}
PString PVXMLSession::GetVar(const PString & ostr) const
{
PString str = ostr;
PString scope;
// get scope
PINDEX pos = str.Find('.');
if (pos != P_MAX_INDEX) {
scope = str.Left(pos);
str = str.Mid(pos+1);
}
// process session scope
if (scope.IsEmpty() || (scope *= "session")) {
if (sessionVars.Contains(str))
return sessionVars(str);
}
// assume any other scope is actually document or application
return documentVars(str);
}
void PVXMLSession::SetVar(const PString & ostr, const PString & val)
{
PString str = ostr;
PString scope;
// get scope
PINDEX pos = str.Find('.');
if (pos != P_MAX_INDEX) {
scope = str.Left(pos);
str = str.Mid(pos+1);
}
// do session scope
if (scope.IsEmpty() || (scope *= "session")) {
sessionVars.SetAt(str, val);
return;
}
PTRACE(3, "PVXML\tDocument: " << str << " = \"" << val << "\"");
// assume any other scope is actually document or application
documentVars.SetAt(str, val);
}
BOOL PVXMLSession::PlayFile(const PString & fn, PINDEX repeat, PINDEX delay, BOOL autoDelete)
{
if (vxmlChannel == NULL || !vxmlChannel->QueueFile(fn, repeat, delay, autoDelete))
return FALSE;
AllowClearCall();
return TRUE;
}
BOOL PVXMLSession::PlayCommand(const PString & cmd, PINDEX repeat, PINDEX delay)
{
if (vxmlChannel == NULL || !vxmlChannel->QueueCommand(cmd, repeat, delay))
return FALSE;
AllowClearCall();
return TRUE;
}
BOOL PVXMLSession::PlayData(const PBYTEArray & data, PINDEX repeat, PINDEX delay)
{
if (vxmlChannel == NULL || !vxmlChannel->QueueData(data, repeat, delay))
return FALSE;
AllowClearCall();
return TRUE;
}
void PVXMLSession::GetBeepData(PBYTEArray & data, unsigned ms)
{
if (vxmlChannel != NULL)
vxmlChannel->GetBeepData(data, ms);
}
BOOL PVXMLSession::PlaySilence(const PTimeInterval & timeout)
{
return PlaySilence((PINDEX)timeout.GetMilliSeconds());
}
BOOL PVXMLSession::PlaySilence(PINDEX msecs)
{
PBYTEArray nothing;
if (vxmlChannel == NULL || !vxmlChannel->QueueData(nothing, 1, msecs))
return FALSE;
AllowClearCall();
return TRUE;
}
BOOL PVXMLSession::PlayResource(const PURL & url, PINDEX repeat, PINDEX delay)
{
if (vxmlChannel == NULL || !vxmlChannel->QueueResource(url, repeat, delay))
return FALSE;
AllowClearCall();
return TRUE;
}
BOOL PVXMLSession::LoadGrammar(PVXMLGrammar * grammar)
{
if (activeGrammar != NULL) {
delete activeGrammar;
activeGrammar = FALSE;
}
activeGrammar = grammar;
return TRUE;
}
BOOL PVXMLSession::PlayText(const PString & _text,
PTextToSpeech::TextType type,
PINDEX repeat,
PINDEX delay)
{
PStringArray list;
BOOL useCache = !(GetVar("caching") *= "safe");
if (!ConvertTextToFilenameList(_text, type, list, useCache) || (list.GetSize() == 0)) {
PTRACE(1, "PVXML\tCannot convert text to speech");
return FALSE;
}
PVXMLPlayableFilenameList * playable = new PVXMLPlayableFilenameList;
if (!playable->Open(*vxmlChannel, list, delay, repeat, !useCache)) {
delete playable;
PTRACE(1, "PVXML\tCannot create playable for filename list");
return FALSE;
}
return vxmlChannel->QueuePlayable(playable);
}
BOOL PVXMLSession::ConvertTextToFilenameList(const PString & _text, PTextToSpeech::TextType type, PStringArray & filenameList, BOOL useCache)
{
PString prefix = psprintf("tts%i", type);
PStringArray lines = _text.Trim().Lines();
for (PINDEX i = 0; i < lines.GetSize(); i++) {
PString text = lines[i].Trim();
if (text.IsEmpty())
continue;
BOOL spoken = FALSE;
PFilePath dataFn;
// see if we have converted this text before
PString contentType;
if (useCache)
spoken = PVXMLCache::GetResourceCache().Get(prefix, text, "wav", contentType, dataFn);
// if not cached, then use the text to speech converter
if (!spoken) {
PFilePath tmpfname;
if (textToSpeech != NULL) {
tmpfname = PVXMLCache::GetResourceCache().GetRandomFilename("tts", "wav");
if (!textToSpeech->OpenFile(tmpfname)) {
PTRACE(2, "PVXML\tcannot open file " << tmpfname);
} else {
spoken = textToSpeech->Speak(text, type);
if (!textToSpeech->Close()) {
PTRACE(2, "PVXML\tcannot close TTS engine");
}
}
textToSpeech->Close();
if (useCache)
PVXMLCache::GetResourceCache().Put(prefix, text, "wav", contentType, tmpfname, dataFn);
else
dataFn = tmpfname;
}
}
if (!spoken) {
PTRACE(2, "PVXML\tcannot speak text using TTS engine");
} else
filenameList.AppendString(dataFn);
}
return filenameList.GetSize() > 0;
}
void PVXMLSession::SetPause(BOOL _pause)
{
if (vxmlChannel != NULL)
vxmlChannel->SetPause(_pause);
}
BOOL PVXMLSession::IsPlaying() const
{
return (vxmlChannel != NULL) && vxmlChannel->IsPlaying();
}
BOOL PVXMLSession::StartRecording(const PFilePath & /*_recordFn*/,
BOOL /*_recordDTMFTerm*/,
const PTimeInterval & /*_recordMaxTime*/,
const PTimeInterval & /*_recordFinalSilence*/)
{
/*
recording = TRUE;
recordFn = _recordFn;
recordDTMFTerm = _recordDTMFTerm;
recordMaxTime = _recordMaxTime;
recordFinalSilence = _recordFinalSilence;
if (incomingChannel != NULL) {
PXMLElement* element = (PXMLElement*) currentNode;
if ( element->HasAttribute("name")) {
PString chanName = element->GetAttribute("name");
incomingChannel->SetName(chanName);
}
return incomingChannel->StartRecording(recordFn, (unsigned )recordFinalSilence.GetMilliSeconds());
}
*/
return FALSE;
}
void PVXMLSession::RecordEnd()
{
if (recording)
recordSync.Signal();
}
BOOL PVXMLSession::EndRecording()
{
if (recording) {
recording = FALSE;
if (vxmlChannel != NULL)
return vxmlChannel->EndRecording();
}
return FALSE;
}
BOOL PVXMLSession::IsRecording() const
{
return (vxmlChannel != NULL) && vxmlChannel->IsRecording();
}
PWAVFile * PVXMLSession::CreateWAVFile(const PFilePath & fn, PFile::OpenMode mode, int opts, unsigned fmt)
{
if (!fn.IsEmpty())
return new PWAVFile(fn, mode, opts, fmt);
return new PWAVFile(mode, opts, fmt);
}
void PVXMLSession::AllowClearCall()
{
allowFinish = TRUE;
}
BOOL PVXMLSession::TraverseAudio()
{
if (!currentNode->IsElement()) {
PlayText(((PXMLData *)currentNode)->GetString());
}
else {
PXMLElement * element = (PXMLElement *)currentNode;
if (element->GetName() *= "value") {
PString className = element->GetAttribute("class");
PString value = EvaluateExpr(element->GetAttribute("expr"));
SayAs(className, value);
}
else if (element->GetName() *= "sayas") {
PString className = element->GetAttribute("class");
PXMLObject * object = element->GetElement();
if (!object->IsElement()) {
PString text = ((PXMLData *)object)->GetString();
SayAs(className, text);
}
}
else if (element->GetName() *= "break") {
// msecs is VXML 1.0
if (element->HasAttribute("msecs"))
PlaySilence(element->GetAttribute("msecs").AsInteger());
// time is VXML 2.0
else if (element->HasAttribute("time")) {
PTimeInterval time = StringToTime(element->GetAttribute("time"));
PlaySilence(time);
}
else if (element->HasAttribute("size")) {
PString size = element->GetAttribute("size");
if (size *= "none")
;
else if (size *= "small")
PlaySilence(SMALL_BREAK_MSECS);
else if (size *= "large")
PlaySilence(LARGE_BREAK_MSECS);
else
PlaySilence(MEDIUM_BREAK_MSECS);
}
// default to medium pause
else {
PlaySilence(MEDIUM_BREAK_MSECS);
}
}
else if (element->GetName() *= "audio") {
BOOL loaded = FALSE;
if (element->HasAttribute("src")) {
PString str = element->GetAttribute("src").Trim();
if (!str.IsEmpty() && (str[0] == '|')) {
loaded = TRUE;
PlayCommand(str.Mid(1));
}
else {
// get a normalised name for the resource
PFilePath fn;
PURL url = NormaliseResourceName(str);
// load the resource from the cache
PString contentType;
BOOL useCache = !(GetVar("caching") *= "safe") && !(element->GetAttribute("caching") *= "safe");
if (RetreiveResource(url, contentType, fn, useCache)) {
PWAVFile * wavFile = vxmlChannel->CreateWAVFile(fn);
if (wavFile == NULL)
PTRACE(3, "PVXML\tCannot create audio file " + fn);
else if (!wavFile->IsOpen())
delete wavFile;
else {
loaded = TRUE;
PlayFile(fn, 0, 0, !useCache); // make sure we delete the file if not cacheing
}
}
}
if (loaded) {
// skip to the next node
if (element->HasSubObjects())
currentNode = element->GetElement(element->GetSize() - 1);
}
}
}
else
PTRACE(3, "PVXML\tUnknown audio tag " << element->GetName() << " encountered");
}
return TRUE;
}
BOOL PVXMLSession::TraverseGoto() // <goto>
{
PAssert(currentNode != NULL, "ProcessGotoElement(): Expected valid node");
if (currentNode == NULL)
return FALSE;
// LATER: handle expr, expritem, fetchaudio, fetchhint, fetchtimeout, maxage, maxstale
PAssert(currentNode->IsElement(), "ProcessGotoElement(): Expected element");
// nextitem
PString nextitem = ((PXMLElement*)currentNode)->GetAttribute("nextitem");
if (!nextitem.IsEmpty()) {
// LATER: Take out the optional #
currentForm = FindForm(nextitem);
currentNode = currentForm;
if (currentForm == NULL) {
// LATER: throw "error.semantic" or "error.badfetch" -- lookup which
return FALSE;
}
return TRUE;
}
// next
PString next = ((PXMLElement*)currentNode)->GetAttribute("next");
// LATER: fixup filename to prepend path
if (!next.IsEmpty()) {
if (next[0] == '#') {
next = next.Right( next.GetLength() -1 );
currentForm = FindForm(next);
currentNode = currentForm;
// LATER: throw "error.semantic" or "error.badfetch" -- lookup which
return currentForm != NULL;
}
else {
PURL url = NormaliseResourceName(next);
return LoadURL(url) && (currentForm != NULL);
}
}
return FALSE;
}
BOOL PVXMLSession::TraverseGrammar() // <grammar>
{
// LATER: A bunch of work to do here!
// For now we only support the builtin digits type and do not parse any grammars.
// NOTE: For now we will process both <grammar> and <field> here.
// NOTE: Later there needs to be a check for <grammar> which will pull
// out the text and process a grammar like '1 | 2'
// Right now we only support one active grammar.
if (activeGrammar != NULL) {
PTRACE(2, "PVXML\tWarning: can only process one grammar at a time, ignoring previous grammar");
delete activeGrammar;
activeGrammar = NULL;
}
PVXMLGrammar * newGrammar = NULL;
// Is this a built-in type?
PString type = ((PXMLElement*)currentNode)->GetAttribute("type");
if (!type.IsEmpty()) {
PStringArray tokens = type.Tokenise("?;", TRUE);
PString builtintype;
if (tokens.GetSize() > 0)
builtintype = tokens[0];
if (builtintype *= "digits") {
PINDEX minDigits(1);
PINDEX maxDigits(100);
// look at each parameter
for (PINDEX i(1); i < tokens.GetSize(); i++) {
PStringArray params = tokens[i].Tokenise("=", TRUE);
if (params.GetSize() == 2) {
if (params[0] *= "minlength") {
minDigits = params[1].AsInteger();
}
else if (params[0] *= "maxlength") {
maxDigits = params[1].AsInteger();
}
else if (params[0] *= "length") {
minDigits = maxDigits = params[1].AsInteger();
}
}
else {
// Invalid parameter skipped
// LATER: throw 'error.semantic'
}
}
newGrammar = new PVXMLDigitsGrammar((PXMLElement*)currentNode, minDigits, maxDigits, "");
}
else {
// LATER: throw 'error.unsupported'
return FALSE;
}
}
if (newGrammar != NULL)
return LoadGrammar(newGrammar);
return TRUE;
}
// Finds the proper event hander for 'noinput', 'filled', 'nomatch' and 'error'
// by searching the scope hiearchy from the current from
PXMLElement * PVXMLSession::FindHandler(const PString & event)
{
PAssert(currentNode->IsElement(), "Expected 'PXMLElement' in PVXMLSession::FindHandler");
PXMLElement * tmp = (PXMLElement *)currentNode;
PXMLElement * handler = NULL;
// Look in all the way up the tree for a handler either explicitly or in a catch
while (tmp != NULL) {
// Check for an explicit hander - i.e. <error>, <filled>, <noinput>, <nomatch>, <help>
if ((handler = tmp->GetElement(event)) != NULL)
return handler;
// Check for a <catch>
if ((handler = tmp->GetElement("catch")) != NULL) {
PString strCond = handler->GetAttribute("cond");
if (strCond.Find(event))
return handler;
}
tmp = tmp->GetParent();
}
return NULL;
}
void PVXMLSession::SayAs(const PString & className, const PString & _text)
{
PString text = _text.Trim();
if (!text.IsEmpty()) {
PTextToSpeech::TextType type = PTextToSpeech::Literal;
if (className *= "digits")
type = PTextToSpeech::Digits;
else if (className *= "literal")
type = PTextToSpeech::Literal;
else if (className *= "number")
type = PTextToSpeech::Number;
else if (className *= "currency")
type = PTextToSpeech::Currency;
else if (className *= "time")
type = PTextToSpeech::Time;
else if (className *= "date")
type = PTextToSpeech::Date;
else if (className *= "phone")
type = PTextToSpeech::Phone;
else if (className *= "ipaddress")
type = PTextToSpeech::IPAddress;
else if (className *= "duration")
type = PTextToSpeech::Duration;
else
PlayText(text, type);
}
}
PTimeInterval PVXMLSession::StringToTime(const PString & str)
{
PTimeInterval timeout;
long msecs = str.AsInteger();
if (str.Find("ms") != P_MAX_INDEX)
;
else if (str.Find("s") != P_MAX_INDEX)
msecs = msecs * 1000;
return PTimeInterval(msecs);
}
BOOL PVXMLSession::TraverseTransfer()
{
PVXMLTransferOptions opts;
PAssert(currentNode != NULL, "TraverseTransfer(): Expected valid node");
if (currentNode == NULL)
return FALSE;
PAssert(currentNode->IsElement(), "TraverseTransfer(): Expected element");
// Retreive parameters
PString dest = ((PXMLElement*)currentNode)->GetAttribute("dest");
PString source = ((PXMLElement*)currentNode)->GetAttribute("source");
PString connectTimeoutStr = ((PXMLElement*)currentNode)->GetAttribute("connecttimeout");
PString bridgeStr = ((PXMLElement*)currentNode)->GetAttribute("dest");
BOOL bridge = bridgeStr *= "true";
PINDEX connectTimeout = connectTimeoutStr.AsInteger();
if ((connectTimeout < 2) && (connectTimeout > 30))
connectTimeout = 30;
if (dest.Find("phone://") == P_MAX_INDEX)
return FALSE;
dest.Delete(0, 8);
if (source.Find("phone://") == P_MAX_INDEX)
return FALSE;
source.Delete(0, 8);
opts.SetCallingToken(callingCallToken );
opts.SetDestinationDNR(dest);
opts.SetSourceDNR(source);
opts.SetTimeout(connectTimeout);
opts.SetBridge(bridge);
DoTransfer(opts);
// Wait for the transfer result signal
transferSync.Wait();
return TRUE;
}
void PVXMLSession::OnTransfer(const PVXMLTransferResult & args)
{
// transfer has ended, save result
SetVar(args.GetName(), args);
// Signal transfer initiator that the transfer has ended and the VXML can
// continue
transferSync.Signal();
}
BOOL PVXMLSession::TraverseIf()
{
// If 'cond' parameter evaluates to true, enter child entities, else
// go to next element.
PString condition = ((PXMLElement*)currentNode)->GetAttribute("cond");
// Find comparison type
PINDEX location = condition.Find("==");
BOOL isEqual = (location < condition.GetSize());
if (isEqual) {
// Find var name
PString varname = condition.Left(location);
// Find value, skip '=' signs
PString cond_value = condition.Right(condition.GetSize() - (location + 3));
// check if var value equals value from condition and if not skip child elements
PString value = GetVar(varname);
if (cond_value == value) {
PTRACE( 3, "VXMLSess\t\tCondition matched \"" << condition << "\"" );
} else {
PTRACE( 3, "VXMLSess\t\tCondition \"" << condition << "\"did not match, " << varname << " == " << value );
if (currentNode->IsElement()) {
PXMLElement* element = (PXMLElement*) currentNode;
if (element->HasSubObjects()) {
// Step to last child element (really last element is NULL?)
currentNode = element->GetElement(element->GetSize() - 1);
}
}
}
}
else {
PTRACE( 1, "\tPVXMLSession, <if> element contains condition with operator other than ==, not implemented" );
return FALSE;
}
return TRUE;
}
BOOL PVXMLSession::TraverseExit()
{
currentNode = NULL;
forceEnd = TRUE;
waitForEvent.Signal();
return TRUE;
}
BOOL PVXMLSession::TraverseSubmit()
{
BOOL result = FALSE;
// Do HTTP client stuff here
// Find out what to submit, for now, only support a WAV file
PXMLElement * element = (PXMLElement *)currentNode;
if (!element->HasAttribute("namelist")){
PTRACE(1, "VXMLSess\t<submit> does not contain \"namelist\" parameter");
return FALSE;
}
PString name = element->GetAttribute("namelist");
if (name.Find(" ") < name.GetSize()) {
PTRACE(1, "VXMLSess\t<submit> does not support more than one value in \"namelist\" parameter");
return FALSE;
}
if (!element->HasAttribute("next")) {
PTRACE(1, "VXMLSess\t<submit> does not contain \"next\" parameter");
return FALSE;
}
PString url = element->GetAttribute("next");
if (url.Find( "http://" ) > url.GetSize()) {
PTRACE(1, "VXMLSess\t<submit> needs a full url as the \"next\" parameter");
return FALSE;
}
if (!(GetVar(name + ".type") == "audio/x-wav" )) {
PTRACE(1, "VXMLSess\t<submit> does not (yet) support submissions of types other than \"audio/x-wav\"");
return FALSE;
}
PString fileName = GetVar(name + ".filename");
if (!(element->HasAttribute("method"))) {
PTRACE(1, "VXMLSess\t<submit> does not (yet) support default method type \"get\"");
return FALSE;
}
if ( !PFile::Exists(fileName )) {
PTRACE(1, "VXMLSess\t<submit> cannot find file " << fileName);
return FALSE;
}
PString fileNameOnly;
int pos = fileName.FindLast( "/" );
if (pos < fileName.GetLength()) {
fileNameOnly = fileName.Right( ( fileName.GetLength() - pos ) - 1 );
}
else {
pos = fileName.FindLast("\\");
if (pos < fileName.GetSize()) {
fileNameOnly = fileName.Right((fileName.GetLength() - pos) - 1);
}
else {
fileNameOnly = fileName;
}
}
PHTTPClient client;
PMIMEInfo sendMIME, replyMIME;
if (element->GetAttribute("method") *= "post") {
// 1 2 3 4123
PString boundary = "--------012345678901234567890123458VXML";
sendMIME.SetAt( PHTTP::ContentTypeTag, "multipart/form-data; boundary=" + boundary);
sendMIME.SetAt( PHTTP::UserAgentTag, "PVXML TraverseSubmit" );
sendMIME.SetAt( "Accept", "text/html" );
// After this all boundaries have a "--" prepended
boundary = "--" + boundary;
// Create the mime header
// First set the primary boundary
PString mimeHeader = boundary + "\r\n";
// Add content disposition
mimeHeader += "Content-Disposition: form-data; name=\"voicemail\"; filename=\"" + fileNameOnly + "\"\r\n";
// Add content type
mimeHeader += "Content-Type: audio/wav\r\n\r\n";
// Create the footer and add the closing of the content with a CR/LF
PString mimeFooter = "\r\n";
// Copy the header, buffer and footer together in one PString
// Load the WAV file into memory
PFile file( fileName, PFile::ReadOnly );
int size = file.GetLength();
PString mimeThing;
// Make PHP happy?
// Anyway, this shows how to add more variables, for when namelist containes more elements
PString mimeMaxFileSize = boundary + "\r\nContent-Disposition: form-data; name=\"MAX_FILE_SIZE\"\r\n\r\n3000000\r\n";
// Finally close the body with the boundary again, but also add "--"
// to show this is the final boundary
boundary = boundary + "--";
mimeFooter += boundary + "\r\n";
mimeHeader = mimeMaxFileSize + mimeHeader;
mimeThing.SetSize( mimeHeader.GetSize() + size + mimeFooter.GetSize() );
// Copy the header to the result
memcpy( mimeThing.GetPointer(), mimeHeader.GetPointer(), mimeHeader.GetLength());
// Copy the contents of the file to the mime result
file.Read( mimeThing.GetPointer() + mimeHeader.GetLength(), size );
// Copy the footer to the result
memcpy( mimeThing.GetPointer() + mimeHeader.GetLength() + size, mimeFooter.GetPointer(), mimeFooter.GetLength());
// Send the POST request to the server
result = client.PostData( url, sendMIME, mimeThing, replyMIME );
// TODO, Later:
// Remove file?
// Load reply from server as new VXML docuemnt ala <goto>
}
else {
if (element->GetAttribute("method") != "get") {
PTRACE(1, "VXMLSess\t<submit> does not (yet) support method type \"" << element->GetAttribute( "method" ) << "\"");
return FALSE;
}
PString getURL = url + "?" + name + "=" + GetVar( name );
client.GetDocument( url, sendMIME, replyMIME );
// TODO, Later:
// Load reply from server as new VXML document ala <goto>
}
if (!result) {
PTRACE( 1, "VXMLSess\t<submit> to server failed with "
<< client.GetLastResponseCode() << " "
<< client.GetLastResponseInfo() );
}
return result;
}
BOOL PVXMLSession::TraverseProperty()
{
PXMLElement* element = (PXMLElement *) currentNode;
if (element->HasAttribute("name"))
SetVar(element->GetAttribute("name"), element->GetAttribute("value"));
return TRUE;
}
BOOL PVXMLSession::TraverseMenu()
{
BOOL result = FALSE;
PVXMLGrammar * newGrammar = new PVXMLDigitsGrammar((PXMLElement*) currentNode, 1, 1, "" );
LoadGrammar(newGrammar);
result = TRUE;
return result;
}
BOOL PVXMLSession::TraverseChoice(const PString & grammarResult)
{
// Iterate over all choice elements starting at currentnode
BOOL result = FALSE;
PXMLElement* element = (PXMLElement *) currentNode;
// Current node is a choice element
PString dtmf = element->GetAttribute( "dtmf" );
if (dtmf.IsEmpty())
dtmf = PString(PString::Unsigned, defaultDTMF);
// Check if DTMF value for grammarResult matches the DTMF value for the choice
if (dtmf == grammarResult) {
// Find the form at next parameter
PString formID = element->GetAttribute( "next" );
PTRACE(2, "VXMLsess\tFound form id " << formID );
if (!formID.IsEmpty()) {
formID = formID.Right( formID.GetLength() - 1 );
currentNode = FindForm( formID );
if (currentNode != NULL)
result = TRUE;
}
}
return result;
}
BOOL PVXMLSession::TraverseVar()
{
BOOL result = FALSE;
PXMLElement* element = (PXMLElement *) currentNode;
PString name = element->GetAttribute( "name" );
PString expr = element->GetAttribute( "expr" );
if (name.IsEmpty() || expr.IsEmpty()) {
PTRACE( 1, "VXMLSess\t<var> has a problem with its parameters, name=\"" << name << "\", expr=\"" << expr << "\"" );
}
else {
SetVar(name, expr);
result = TRUE;
}
return result;
}
void PVXMLSession::OnEndRecording(const PString & /*channelName*/)
{
//SetVar(channelName + ".size", PString(incomingChannel->GetWAVFile()->GetDataLength() ) );
//SetVar(channelName + ".type", "audio/x-wav" );
//SetVar(channelName + ".filename", incomingChannel->GetWAVFile()->GetName() );
}
void PVXMLSession::Trigger()
{
waitForEvent.Signal();
}
/////////////////////////////////////////////////////////////////////////////////////////
PVXMLGrammar::PVXMLGrammar(PXMLElement * _field)
: field(_field), state(PVXMLGrammar::NOINPUT)
{
}
//////////////////////////////////////////////////////////////////
PVXMLMenuGrammar::PVXMLMenuGrammar(PXMLElement * _field)
: PVXMLGrammar(_field)
{
}
//////////////////////////////////////////////////////////////////
PVXMLDigitsGrammar::PVXMLDigitsGrammar(PXMLElement * _field, PINDEX _minDigits, PINDEX _maxDigits, PString _terminators)
: PVXMLGrammar(_field),
minDigits(_minDigits),
maxDigits(_maxDigits),
terminators(_terminators)
{
PAssert(_minDigits <= _maxDigits, "Error - invalid grammar parameter");
}
BOOL PVXMLDigitsGrammar::OnUserInput(const char ch)
{
// Ignore any other keys if we've already filled the grammar
if (state == PVXMLGrammar::FILLED || state == PVXMLGrammar::NOMATCH)
return TRUE;
// is this char the terminator?
if (terminators.Find(ch) != P_MAX_INDEX) {
state = (value.GetLength() >= minDigits && value.GetLength() <= maxDigits) ?
PVXMLGrammar::FILLED :
PVXMLGrammar::NOMATCH;
return TRUE;
}
// Otherwise add to the grammar and check to see if we're done
value += ch;
if (value.GetLength() == maxDigits) {
state = PVXMLGrammar::FILLED; // the grammar is filled!
return TRUE;
}
return FALSE;
}
void PVXMLDigitsGrammar::Stop()
{
// Stopping recognition here may change the state if something was
// recognized but it didn't fill the number of digits requested
if (!value.IsEmpty())
state = PVXMLGrammar::NOMATCH;
// otherwise the state will stay as NOINPUT
}
//////////////////////////////////////////////////////////////////
PVXMLChannel::PVXMLChannel(unsigned _frameDelay, PINDEX frameSize)
: PDelayChannel(DelayReadsAndWrites, _frameDelay, frameSize)
{
vxmlInterface = NULL;
sampleFrequency = 8000;
closed = FALSE;
recording = FALSE;
recordable = NULL;
playing = FALSE;
silentCount = 20; // wait 20 frames before playing the OGM
paused = FALSE;
currentPlayItem = NULL;
}
BOOL PVXMLChannel::Open(PVXMLChannelInterface * _vxmlInterface)
{
currentPlayItem = NULL;
vxmlInterface = _vxmlInterface;
return TRUE;
}
PVXMLChannel::~PVXMLChannel()
{
Close();
}
BOOL PVXMLChannel::IsOpen() const
{
return !closed;
}
BOOL PVXMLChannel::Close()
{
if (!closed) {
EndRecording();
FlushQueue();
closed = TRUE;
PDelayChannel::Close();
}
return TRUE;
}
PString PVXMLChannel::AdjustWavFilename(const PString & ofn)
{
if (wavFilePrefix.IsEmpty())
return ofn;
PString fn = ofn;
// add in suffix required for channel format, if any
PINDEX pos = ofn.FindLast('.');
if (pos == P_MAX_INDEX) {
if (fn.Right(wavFilePrefix.GetLength()) != wavFilePrefix)
fn += wavFilePrefix;
}
else {
PString basename = ofn.Left(pos);
PString ext = ofn.Mid(pos+1);
if (basename.Right(wavFilePrefix.GetLength()) != wavFilePrefix)
basename += wavFilePrefix;
fn = basename + "." + ext;
}
return fn;
}
PWAVFile * PVXMLChannel::CreateWAVFile(const PFilePath & fn, BOOL recording)
{
PWAVFile * wav = PWAVFile::format(mediaFormat);
if (wav == NULL) {
PTRACE(1, "VXML\tWAV file format " << mediaFormat << " not known");
return NULL;
}
wav->SetAutoconvert();
if (!wav->Open(AdjustWavFilename(fn),
recording ? PFile::WriteOnly : PFile::ReadOnly,
PFile::ModeDefault))
PTRACE(1, "VXML\tCould not open WAV file " << wav->GetName());
else if (recording) {
wav->SetChannels(1);
wav->SetSampleRate(8000);
wav->SetSampleSize(16);
return wav;
}
else if (!wav->IsValid())
PTRACE(1, "VXML\tWAV file header invalid for " << wav->GetName());
else if (wav->GetSampleRate() != sampleFrequency)
PTRACE(1, "VXML\tWAV file has unsupported sample frequency " << wav->GetSampleRate());
else if (wav->GetChannels() != 1)
PTRACE(1, "VXML\tWAV file has unsupported channel count " << wav->GetChannels());
else {
wav->SetAutoconvert(); /// enable autoconvert
PTRACE(4, "VXML\tOpened WAV file " << wav->GetName());
return wav;
}
delete wav;
return NULL;
}
BOOL PVXMLChannel::Write(const void * buf, PINDEX len)
{
if (closed)
return FALSE;
channelWriteMutex.Wait();
// let the recordable do silence detection
if (recordable != NULL && recordable->OnFrame(IsSilenceFrame(buf, len))) {
PTRACE(1, "VXML\tRecording finished due to silence");
EndRecording();
}
// if nothing is capturing incoming data, then fake the timing and return
if ((recordable == NULL) && (GetBaseWriteChannel() == NULL)) {
lastWriteCount = len;
channelWriteMutex.Signal();
PDelayChannel::Wait(len, nextWriteTick);
return TRUE;
}
// write the data and do the correct delay
if (!WriteFrame(buf, len))
EndRecording();
else
totalData += lastWriteCount;
channelWriteMutex.Signal();
return TRUE;
}
BOOL PVXMLChannel::StartRecording(const PFilePath & fn, unsigned _finalSilence, unsigned _maxDuration)
{
PVXMLRecordableFilename * recordable = new PVXMLRecordableFilename();
if (!recordable->Open(fn)) {
delete recordable;
return FALSE;
}
recordable->SetFinalSilence(_finalSilence);
recordable->SetMaxDuration(_maxDuration);
return QueueRecordable(recordable);
}
BOOL PVXMLChannel::QueueRecordable(PVXMLRecordable * newItem)
{
totalData = 0;
// shutdown any existing recording
EndRecording();
// insert the new recordable
PWaitAndSignal mutex(channelWriteMutex);
recordable = newItem;
recording = TRUE;
totalData = 0;
newItem->OnStart();
newItem->Record(*this);
SetReadTimeout(frameDelay);
return TRUE;
}
BOOL PVXMLChannel::EndRecording()
{
PWaitAndSignal mutex(channelWriteMutex);
if (recordable != NULL) {
PTRACE(3, "PVXML\tFinished recording " << totalData << " bytes");
PDelayChannel::Close();
recordable->OnStop();
delete recordable;
recordable = NULL;
PTRACE(3, "PVXML\tRecording finished");
}
return TRUE;
}
BOOL PVXMLChannel::Read(void * buffer, PINDEX amount)
{
// assume we are returning silence
BOOL done = FALSE;
BOOL silenceStuff = FALSE;
BOOL delayDone = FALSE;
while (!done && !silenceStuff) {
if (closed)
return FALSE;
{
PWaitAndSignal m(channelReadMutex);
// if we are paused or in a delay, then do return silence
if (paused || delayTimer.IsRunning()) {
silenceStuff = TRUE;
break;
}
// if we are returning silence frames, then decrement the frame count
// and continue returning silence
if (silentCount > 0) {
silentCount--;
silenceStuff = TRUE;
break;
}
// try and read data from the underlying channel
if (GetBaseReadChannel() != NULL) {
PWaitAndSignal m(queueMutex);
// see if the item needs to repeat
PAssert(currentPlayItem != NULL, "current VXML play item disappeared");
// if the read succeeds, we are done
if (currentPlayItem->ReadFrame(*this, buffer, amount)) {
totalData += amount;
delayDone = TRUE;
done = TRUE;
break;
}
// if a timeout, send silence
if (GetErrorCode(LastReadError) == Timeout) {
silenceStuff = TRUE;
break;
}
PTRACE(3, "PVXML\tFinished playing " << totalData << " bytes");
PDelayChannel::Close();
// repeat the item if needed
if (currentPlayItem->GetRepeat() > 1) {
currentPlayItem->SetRepeat(currentPlayItem->GetRepeat()-1);
currentPlayItem->OnRepeat(*this);
continue;
}
// see if end of queue delay specified
PINDEX delay = 0;
if (currentPlayItem->delayDone) {
delay = currentPlayItem->GetDelay();
if (delay != 0) {
PTRACE(3, "PVXML\tDelaying for " << delay);
delayTimer = delay;
currentPlayItem->delayDone = TRUE;
continue;
}
}
// stop the current item
currentPlayItem->OnStop();
delete currentPlayItem;
currentPlayItem = NULL;
}
// check the queue for the next action
{
PWaitAndSignal m(queueMutex);
// if nothing in the queue (which is weird as something just stopped playing)
// then trigger the VXML and send silence
currentPlayItem = playQueue.Dequeue();
if (currentPlayItem == NULL) {
vxmlInterface->Trigger();
silenceStuff = TRUE;
break;
}
// start the new item
currentPlayItem->OnStart();
currentPlayItem->Play(*this);
SetReadTimeout(frameDelay);
totalData = 0;
}
}
}
// start silence frame if required
// note that this always requires a delay
if (silenceStuff) {
lastReadCount = CreateSilenceFrame(buffer, amount);
}
// make sure we always do the correct delay
if (!delayDone)
Wait(amount, nextReadTick);
return TRUE;
}
BOOL PVXMLChannel::QueuePlayable(const PString & type,
const PString & arg,
PINDEX repeat,
PINDEX delay,
BOOL autoDelete)
{
PTRACE(3, "PVXML\tEnqueueing playable " << type << " with arg " << arg << " for playing");
PVXMLPlayable * item = PFactory<PVXMLPlayable>::CreateInstance(type);
if (item == NULL) {
PTRACE(1, "VXML\tCannot find playable of type " << type);
delete item;
return FALSE;
}
if (!item->Open(*this, arg, delay, repeat, autoDelete)) {
PTRACE(1, "VXML\tCannot open playable of type " << type << " with arg " << arg);
delete item;
return FALSE;
}
if (QueuePlayable(item))
return TRUE;
delete item;
return FALSE;
}
BOOL PVXMLChannel::QueuePlayable(PVXMLPlayable * newItem)
{
newItem->SetSampleFrequency(sampleFrequency);
PWaitAndSignal mutex(queueMutex);
playQueue.Enqueue(newItem);
return TRUE;
}
BOOL PVXMLChannel::QueueResource(const PURL & url, PINDEX repeat, PINDEX delay)
{
if (url.GetScheme() *= "file")
return QueuePlayable("File", url.AsFilePath(), repeat, delay, FALSE);
else
return QueuePlayable("URL", url.AsString(), repeat, delay);
}
BOOL PVXMLChannel::QueueData(const PBYTEArray & data, PINDEX repeat, PINDEX delay)
{
PTRACE(3, "PVXML\tEnqueueing " << data.GetSize() << " bytes for playing");
PVXMLPlayableData * item = dynamic_cast<PVXMLPlayableData *>(PFactory<PVXMLPlayable>::CreateInstance("PCM Data"));
if (item == NULL) {
PTRACE(1, "VXML\tCannot find playable of type 'PCM Data'");
delete item;
return FALSE;
}
if (!item->Open(*this, "", delay, repeat, TRUE)) {
PTRACE(1, "VXML\tCannot open playable of type 'PCM Data'");
delete item;
return FALSE;
}
if (QueuePlayable(item))
return TRUE;
delete item;
return FALSE;
}
void PVXMLChannel::FlushQueue()
{
PWaitAndSignal mutex(channelReadMutex);
if (GetBaseReadChannel() != NULL)
PDelayChannel::Close();
PWaitAndSignal m(queueMutex);
PVXMLPlayable * qItem;
while ((qItem = playQueue.Dequeue()) != NULL) {
qItem->OnStop();
delete qItem;
}
if (currentPlayItem != NULL) {
currentPlayItem->OnStop();
delete currentPlayItem;
currentPlayItem = NULL;
}
}
///////////////////////////////////////////////////////////////
class PVXMLChannelPCM : public PVXMLChannel
{
PCLASSINFO(PVXMLChannelPCM, PVXMLChannel);
public:
PVXMLChannelPCM();
protected:
// overrides from PVXMLChannel
virtual BOOL WriteFrame(const void * buf, PINDEX len);
virtual BOOL ReadFrame(void * buffer, PINDEX amount);
virtual PINDEX CreateSilenceFrame(void * buffer, PINDEX amount);
virtual BOOL IsSilenceFrame(const void * buf, PINDEX len) const;
virtual void GetBeepData(PBYTEArray & data, unsigned ms);
};
PFactory<PVXMLChannel>::Worker<PVXMLChannelPCM> pcmVXMLChannelFactory(VXML_PCM16);
PVXMLChannelPCM::PVXMLChannelPCM()
: PVXMLChannel(30, 480)
{
mediaFormat = VXML_PCM16;
wavFilePrefix = PString::Empty();
}
BOOL PVXMLChannelPCM::WriteFrame(const void * buf, PINDEX len)
{
return PDelayChannel::Write(buf, len);
}
BOOL PVXMLChannelPCM::ReadFrame(void * buffer, PINDEX amount)
{
PINDEX len = 0;
while (len < amount) {
if (!PDelayChannel::Read(len + (char *)buffer, amount-len))
return FALSE;
len += GetLastReadCount();
}
return TRUE;
}
PINDEX PVXMLChannelPCM::CreateSilenceFrame(void * buffer, PINDEX amount)
{
memset(buffer, 0, amount);
return amount;
}
BOOL PVXMLChannelPCM::IsSilenceFrame(const void * buf, PINDEX len) const
{
// Calculate the average signal level of this frame
int sum = 0;
const short * pcm = (const short *)buf;
const short * end = pcm + len/2;
while (pcm != end) {
if (*pcm < 0)
sum -= *pcm++;
else
sum += *pcm++;
}
// calc average
unsigned level = sum / (len / 2);
return level < 500; // arbitrary level
}
static short beepData[] = { 0, 18784, 30432, 30400, 18784, 0, -18784, -30432, -30400, -18784 };
void PVXMLChannelPCM::GetBeepData(PBYTEArray & data, unsigned ms)
{
data.SetSize(0);
while (data.GetSize() < (PINDEX)((ms * 8) / 2)) {
PINDEX len = data.GetSize();
data.SetSize(len + sizeof(beepData));
memcpy(len + data.GetPointer(), beepData, sizeof(beepData));
}
}
///////////////////////////////////////////////////////////////
class PVXMLChannelG7231 : public PVXMLChannel
{
PCLASSINFO(PVXMLChannelG7231, PVXMLChannel);
public:
PVXMLChannelG7231();
// overrides from PVXMLChannel
virtual BOOL WriteFrame(const void * buf, PINDEX len);
virtual BOOL ReadFrame(void * buffer, PINDEX amount);
virtual PINDEX CreateSilenceFrame(void * buffer, PINDEX amount);
virtual BOOL IsSilenceFrame(const void * buf, PINDEX len) const;
};
PFactory<PVXMLChannel>::Worker<PVXMLChannelG7231> g7231VXMLChannelFactory(VXML_G7231);
PVXMLChannelG7231::PVXMLChannelG7231()
: PVXMLChannel(30, 0)
{
mediaFormat = VXML_G7231;
wavFilePrefix = "_g7231";
}
static const PINDEX g7231Lens[] = { 24, 20, 4, 1 };
BOOL PVXMLChannelG7231::WriteFrame(const void * buffer, PINDEX actualLen)
{
PINDEX len = g7231Lens[(*(BYTE *)buffer)&3];
if (len > actualLen)
return FALSE;
return PDelayChannel::Write(buffer, len);
}
BOOL PVXMLChannelG7231::ReadFrame(void * buffer, PINDEX /*amount*/)
{
if (!PDelayChannel::Read(buffer, 1))
return FALSE;
PINDEX len = g7231Lens[(*(BYTE *)buffer)&3];
if (len != 1) {
if (!PIndirectChannel::Read(1+(BYTE *)buffer, len-1))
return FALSE;
lastReadCount++;
}
return TRUE;
}
PINDEX PVXMLChannelG7231::CreateSilenceFrame(void * buffer, PINDEX /* len */)
{
((BYTE *)buffer)[0] = 2;
memset(((BYTE *)buffer)+1, 0, 3);
return 4;
}
BOOL PVXMLChannelG7231::IsSilenceFrame(const void * buf, PINDEX len) const
{
if (len == 4)
return TRUE;
if (buf == NULL)
return FALSE;
return ((*(const BYTE *)buf)&3) == 2;
}
///////////////////////////////////////////////////////////////
class PVXMLChannelG729 : public PVXMLChannel
{
PCLASSINFO(PVXMLChannelG729, PVXMLChannel);
public:
PVXMLChannelG729();
// overrides from PVXMLChannel
virtual BOOL WriteFrame(const void * buf, PINDEX len);
virtual BOOL ReadFrame(void * buffer, PINDEX amount);
virtual PINDEX CreateSilenceFrame(void * buffer, PINDEX amount);
virtual BOOL IsSilenceFrame(const void * buf, PINDEX len) const;
};
PFactory<PVXMLChannel>::Worker<PVXMLChannelG729> g729VXMLChannelFactory(VXML_G729);
PVXMLChannelG729::PVXMLChannelG729()
: PVXMLChannel(10, 0)
{
mediaFormat = VXML_G729;
wavFilePrefix = "_g729";
}
BOOL PVXMLChannelG729::WriteFrame(const void * buf, PINDEX /*len*/)
{
return PDelayChannel::Write(buf, 10);
}
BOOL PVXMLChannelG729::ReadFrame(void * buffer, PINDEX /*amount*/)
{
return PDelayChannel::Read(buffer, 10);
}
PINDEX PVXMLChannelG729::CreateSilenceFrame(void * buffer, PINDEX /* len */)
{
memset(buffer, 0, 10);
return 10;
}
BOOL PVXMLChannelG729::IsSilenceFrame(const void * /*buf*/, PINDEX /*len*/) const
{
return FALSE;
}
#endif // P_EXPAT
///////////////////////////////////////////////////////////////
syntax highlighted by Code2HTML, v. 0.9.1