if (typeof(WebSpeech) === "undefined") {
    var WebSpeech = {
      version: '4.2',
    //  server: 'http://wa.eguidedog.net/cgi-bin/espeak.pl',
      server: 'http://wa.eguidedog.net/cgi-bin/ekho.pl',
    //  http://webanywhere.cs.washington.edu/cgi-bin/ivona/getsound.pl
    //  server: 'http://localhost/perl/ivona_cgi.pl',
      serverReturnType: 'MP3',
    //  server: 'http://localhost/WebSpeech/ivona.php',
    //  serverReturnType: 'URL', // for IVONA SaaS server proxy
      voice: 'EkhoMandarin',
    //  voice: 'en',
      speedDelta: 0,
      pitchDelta: 0,
      volumeDelta: 0,
     
      sm2ready: false,
      speechQueue: [],
      frameStack: [],
      state: 'NONE', // 'SPEAKHTML', 'SPEAK_ONE_CLAUSE', 'SPEAK_PARAGRAPH', 'SPEAK_HEADING', 'SPEAK_FOCUSABLE'
      rootNode: null, // root node of speakHtml
      curNode: null,
      curPos: 0,
      curText: '',
      curSpeech: null,
      foundNewParagraph: false,
      headingTags: 'H1 H2 H3 H4 H5 H6',
      foundNewHeading: false,
      paragraphTags: 'P DIV H1 H2 H3 H4 H5 H6 HR BR',
      preHighLightNode: null,
      highLightNode: null,
      seperators: [".",
        "。", // Chinese full stop (12290, \u3002)
        ",",
        "，", // Chinese comma
        ";",
        "；", // Chinese semi-colon
        "(",
        "（", // Chinese left brace
        ")",
        "）", // Chinese right brace
        "\n"],
     
      cacheNode: null,
      cachePos: 0,
      cacheCount: 0,
      cacheMaxCount: 5, // number of speech to pre-cache
     
      isCtrlKeyDown: false,
      isShiftKeyDown: false,
     
      htmlStartId: null, // element ID for speakHtml. if null, <body> will be used
     
      onready: function () {
        this.sm2ready = true;
        WebSpeech.playNextSpeech();
      },
      ready: function (f) {
        this.onready = function () {
          this.sm2ready = true;
          f();
        }
      },
      onfinish: function () {},
     
      text2url: {}, // hash of text (md5 with prefix 's') to sound file url
      text2audio: {}, // hash or text (md5 with prefix 's') to SM object
     
      debug: true,
      log: function (msg) {
        // there should be such element in page: <div id='WebSpeechLog'></div>
        var logDiv = document.getElementById('WebSpeechLog');
        if (logDiv && this.debug) {
          logDiv.innerHTML += msg + '<br/>';
        }
      },
     
      getStyle: function (elem, styleName) {
        if (elem.nodeType !== 1) { // Element.nodeType === 1
          return null;
        }
     
        try {
          if (elem.currentStyle) {
            return elem.currentStyle[styleName];
          } else if (window.getComputedStyle) {
            return window.getComputedStyle(elem, null).getPropertyValue(styleName);
          }
        } catch (e) {
          return null;
        }
      },
     
      getUrl: function (cmd, text) {
        return this.server + '?cmd=' + cmd + '&voice=' +
          this.voice + '&speedDelta=' + this.speedDelta + '&pitchDelta=' +
          this.pitchDelta + '&volumeDelta=' + this.volumeDelta + '&text=' +
          encodeURIComponent(text); // TODO: change it to encodeURIComponent(text); and decode with `$text = urldecode($_GET["text"]);` on server side
      },
     
      playNextSpeech: function () {
        var text, speechId, url, speechRef;
     
        if (this.voice === 'None') {
          this.speechQueue = [];
          return;
        }
     
        if (!soundManager.supported()) {
          return;
        }
     
        if (!this.sm2ready) {
          soundManager.onready(function() { WebSpeech.playNextSpeech(); });
          return;
        }
     
        // no speech left
        if (this.speechQueue.length === 0) {
          if (this.state === 'SPEAKHTML') {
            text = this.getNextClause();
            if (text) {
              speechId = this.getSpeechId(text);
              this.speechQueue.push(speechId);
              if (!this.text2url[speechId]) {
                url = this.getUrl('SPEAK', text);
                if (this.serverReturnType === 'URL') {
                  speechRef = this;
                  OX.AJAST.call(url, 'callback', function (isSuccess, result) {
                    if (isSuccess) {
                      speechRef.text2url[speechId] = result;
                      speechRef.playNextSpeech();
                    }
                  });
                } else {
                  this.text2url[speechId] = url;
                }
              }
              this.playNextSpeech();
            } else {
              this.onfinish();
            }
          } else {
            this.onfinish();
          }
          return;
        } else if (!this.text2url[this.speechQueue[0]]) {
          // audio url is not ready
          return;
        }
     
        speechId = this.speechQueue.shift();
     
        if (!this.text2audio[speechId]) {
          this.text2audio[speechId] = soundManager.createSound({
            id: speechId,
            url: this.text2url[speechId],
            multiShot: true/*,
            onconnect: function(bConnect) {
              // this.connected can also be used
              soundManager._writeDebug(this.id+' connected: '+(bConnect?'true':'false'));
            },
            ondataerror: function() {
              soundManager._writeDebug('dataerroe');
            },
            whileloading: function() {
              soundManager._writeDebug('sound '+this.id+' loading, '+this.bytesLoaded+' of '+this.bytesTotal);
            },
            whileplaying: function() {
              soundManager._writeDebug('sound '+this.id+' playing, '+this.position+' of '+this.duration);
            }*/
          });
        }
     
        if (this.state !== 'NONE') {
          this.highLightCurText();
        }
     
        speechRef = this;
        soundManager._writeDebug('play ' + speechId);
        //this.text2audio[speechId].load();
        this.text2audio[speechId].play({
          onfinish: function () {
            speechRef.playNextSpeech();
          }
        });
     
        this.curSpeech = this.text2audio[speechId];
        if (this.cacheCount > 0) {
          this.cacheCount -= 1;
        }
        this.cacheNextSpeech();
      },
     
      // push speech into queue and play on sequence
      speak: function (text) {
        var speechId, speechRef, url;
     
        speechId = this.getSpeechId(text);
        this.speechQueue.push(speechId);
        url = this.getUrl('SPEAK', text);
        if (!this.text2url[speechId]) {
          if (this.serverReturnType === 'URL') {
            speechRef = this;
            OX.AJAST.call(url, 'callback', function (isSuccess, result) {
              if (isSuccess) {
                speechRef.text2url[speechId] = result;
                speechRef.playNextSpeech();
              }
            });
          } else {
            this.text2url[speechId] = url;
            this.playNextSpeech();
          }
        } else {
          this.playNextSpeech();
        }
      },
     
      getSpeechId: function (text) {
        return 's' + this.voice + 's' + this.speedDelta + 'p' + this.pitchDelta +
            'v' + this.volumeDelta + MD5(text);
      },
     
      disableHighLight: function () {
        var parentNode;
     
        // disable previous high light
        if (this.highLightNode) {
          parentNode = this.highLightNode.parentNode;
          parentNode.replaceChild(this.preHighLightNode, this.highLightNode);
          if (this.curNode.parentNode === this.highLightNode) {
            if (this.curNode.previousSibling) {
              if (this.curNode.previousSibling.tagName === 'SPAN') {
                this.curPos += this.curNode.previousSibling.firstChild.length;
                if (this.curNode.previousSibling.previousSibling) {
                  this.curPos += this.curNode.previousSibling.previousSibling.length;
                }
              } else {
                this.curPos += this.curNode.previousSibling.length;
              }
            }
            this.curNode = this.preHighLightNode;
          } else if (this.curNode.parentNode &&
              this.curNode.parentNode.parentNode === this.highLightNode) {
            this.curNode = this.preHighLightNode;
          }
          this.preHighLightNode = null;
          this.highLightNode = null;
        }
      },
     
      highLightCurText: function () {
        var part1, part2, part3, parentNode;
     
        this.disableHighLight();
     
        if (!this.curNode.nodeValue)
          return;
     
        // high light new node
        part1 = this.curNode.nodeValue.substr(0, this.curPos - this.curText.length);
        part2 = '<span id="WebSpeechHighLight" style="background-color:#ff0; color:#000">' + this.curText + '</span>';
        part3 = this.curNode.nodeValue.substr(this.curPos);
        this.highLightNode = document.createElement('span');
        this.highLightNode.innerHTML = part1 + part2 + part3;
        parentNode = this.curNode.parentNode;
        this.preHighLightNode = this.curNode;
        parentNode.replaceChild(this.highLightNode, this.curNode);
        if (this.highLightNode.childNodes[0].tagName === 'SPAN') {
          this.curNode = this.highLightNode.childNodes[0].firstChild;
        } else {
          this.curNode = this.highLightNode.childNodes[1].firstChild;
        }
      },
     
      cacheSpeech: function (speechId) {
        var speechRef;
     
        if (this.voice === 'None' || !soundManager.supported()) {
          return;
        }
     
        if (!this.text2audio[speechId]) {
          speechRef = this;
          this.text2audio[speechId] = soundManager.createSound({
            id: speechId,
            url: this.text2url[speechId],
            multiShot: true,
            onfinish: function () {
              speechRef.cacheNextSpeech();
            }
          });
        } else {
          this.cacheNextSpeech();
        }
      },
     
      cacheNextSpeech: function () {
        var curNode, curPos, curText, text, speechId, url, speechRef;
     
        if (this.cacheCount >= this.cacheMaxCount) {
          return;
        }
        this.cacheCount += 1;
     
        // backup current position
        curNode = this.curNode;
        curPos = this.curPos;
        curText = this.curText;
     
        text = this.getNextClause();
        if (text) {
          speechId = this.getSpeechId(text);
          url = this.getUrl('SPEAK', text);
          if (!this.text2url[speechId]) {
            if (this.serverReturnType === 'URL') {
              speechRef = this;
              OX.AJAST.call(url, 'callback', function (isSuccess, result) {
                if (isSuccess) {
                  speechRef.text2url[speechId] = result;
                  speechRef.cacheSpeech(speechId);
                }
              });
            } else {
              this.text2url[speechId] = url;
              this.cacheSpeech(speechId);
            }
          } else {
            this.cacheSpeech(speechId);
          }
        }
     
        // restore current position
        this.curText = curText;
        this.curPos = curPos;
        this.curNode = curNode;
      },
     
      setHtmlStartId: function (id) {
        this.htmlStartId = id;
      },
     
      speakHtml: function (id) {
        var text, speechId, url, speechRef;
     
        this.speechQueue = [];
     
        if (this.voice === 'None') {
          return;
        }
     
        this.state = 'SPEAKHTML';
     
        if (id) {
          this.rootNode = document.getElementById(id);
        } else if (this.htmlStartId) {
          this.rootNode = document.getElementById(this.htmlStartId);
        } else {
          this.rootNode = document.body;
        }
     
        this.curNode = this.rootNode;
        this.curPos = 0;
     
        // prepare the first speech
        text = this.getNextClause();
        if (!text) {
          return;
        }
        this.cacheNode = this.curNode;
        this.cachePos = this.curPos;
        this.speak(text);
     
        // prepare next speech
        this.cacheNextSpeech();
      },
     
      speakPreviousClause: function () {
        var text, speechId, url;
     
        this.stopHtml();
        this.state = 'SPEAK_ONE_CLAUSE';
        text = this.getPreviousClause(); // this is current clause
        text = this.getPreviousClause(); // this is real previous clause
        this.curPos += text.length;
        if (!text) {
          return;
        }
        this.cacheNode = this.curNode;
        this.cachePos = this.curPos;
        this.speak(text);
      },
     
      speakNextClause: function () {
        var text;
     
        this.stopHtml();
        this.state = 'SPEAK_ONE_CLAUSE';
        text = this.getNextClause();
        if (!text) {
          return;
        }
        this.cacheNode = this.curNode;
        this.cachePos = this.curPos;
        this.speak(text);
     
        // prepare next speech
        this.cacheNextSpeech();
      },
     
      speakNextParagraph: function () {
        var text;
     
        this.stopHtml();
        this.state = 'SPEAK_PARAGRAPH';
        this.foundNewParagraph = false;
     
        do {
          text = this.getNextClause();
        } while (!this.foundNewParagraph && text);
     
        if (!this.foundNewParagraph) {
          text = 'No more paragraphs.';
        }
     
        this.speak(text);
      },
     
      speakPreviousParagraph: function () {
        var text;
     
        this.stopHtml();
        this.state = 'SPEAK_PARAGRAPH';
     
        this.getPreviousClause();
        this.getPreviousClause();
        this.foundNewParagraph = false;
        do {
          text = this.getPreviousClause();
        } while (!this.foundNewParagraph && text);
     
        if (!this.foundNewParagraph) {
          text = 'No more paragraphs.';
        } else {
          text = this.getNextClause();
          text = this.getNextClause();
        }
     
        this.speak(text);
      },
     
      speakNextHeading: function () {
        var text;
     
        this.stopHtml();
        this.state = 'SPEAK_HEADING';
        this.foundNewHeading = false;
     
        do {
          text = this.getNextClause();
        } while (!this.foundNewHeading && text);
     
        if (!this.foundNewHeading) {
          text = 'No more headings.';
        }
     
        this.speak(text);
      },
     
      speakPreviousHeading: function () {
        var text;
     
        this.stopHtml();
        this.state = 'SPEAK_HEADING';
     
        this.getPreviousClause();
        this.getPreviousClause();
        this.foundNewHeading = false;
        do {
          text = this.getPreviousClause();
        } while (!this.foundNewHeading && text);
     
        if (!this.foundNewHeading) {
          text = 'No more headings.';
        } else if (!text) {
          // the heading is on top of content
          text = this.getNextClause();
        } else {
          text = this.getNextClause();
          text = this.getNextClause();
        }
     
        this.speak(text);
      },
     
      getFirstClause: function (text) {
        var i;
        for (i in this.seperators) {
          text = text.split(this.seperators[i], 2)[0];
        }
        text = text.replace(/(^\s*)|(\s*$)/g, '');
        return text;
      },
     
      getLastClause: function (text) {
        var i, clauses;
        for (i in this.seperators) {
          clauses = text.split(this.seperators[i]);
          text = clauses[clauses.length - 1];
        }
        text = text.replace(/(^\s*)|(\s*$)/g, '');
        return text;
      },
     
      getPreviousClause: function () {
        var text;
        if (!this.curNode) {
          return '';
        }
     
        if (this.curNode.nodeName === 'FRAME' ||
            this.curNode.nodeName === 'IFRAME') {
          if (this.curNode.contentDocument) {
            this.frameStack.push(this.curNode);
            this.curNode = this.curNode.contentDocument.body;
          }
        }
         
        if (this.curNode.nodeName === 'NOFRAMES' ||
            this.curNode.nodeName === '#comment' ||
            this.curNode.nodeName === 'NOSCRIPT' ||
            this.getStyle(this.curNode, 'display') === 'none' ||
            this.getStyle(this.curNode, 'visibility') === 'hidden') {
          text = ''; // this is meaningless only for passing JSLint
        } else if (!this.curNode.hasChildNodes()) {
          // text node, try to get text to speak
          if (this.curNode.nodeValue) {
            text = this.curNode.nodeValue.substr(0, this.curPos);
            if (text.length > 0) {
              text = this.getLastClause(text);
              if (text.length > 0) {
                this.curPos = this.curNode.nodeValue.lastIndexOf(text, this.curPos - 1);
                // this is it!
                this.curText = text;
                return text;
              }
            }
     
            do {
              this.curPos -= 1;
            } while (this.curPos >= 0 &&
                this.curNode.nodeValue.charAt(this.curPos).match(/\s/));
            if (this.curPos >= 0) {
              return this.getPreviousClause();
            }
          }
        } else {
          while (this.curNode.lastChild) {
            this.curNode = this.curNode.lastChild;
          }
          this.curPos = this.curNode.length;
          this.curText = '';
          return this.getPreviousClause();
        }
     
        if (this.curNode.previousSibling && this.curNode !== this.rootNode) {
          // turn to previous sibling
          this.curNode = this.curNode.previousSibling;
          this.curPos = this.curNode.length;
          this.curText = '';
          return this.getPreviousClause();
        } else {
          // turn to parent node's sibling
          do {
            if (!this.curNode.parentNode) {
              if (this.curNode.nodeName === '#document' &&
                  this.frameStack.length > 0) {
                this.curNode = this.frameStack.pop();
              } else {
                this.log('no parent');
                this.curNode = this.rootNode; // something wrong
              }
            } else {
              this.curNode = this.curNode.parentNode;
     
              if (this.headingTags.indexOf(this.curNode.nodeName) >= 0) { 
                this.foundNewHeading = true;
              }
     
              if (this.paragraphTags.indexOf(this.curNode.nodeName) >= 0) {
                this.foundNewParagraph = true;
              }
            }
          } while (this.curNode !== this.rootNode && (!this.curNode.previousSibling));
     
          this.curPos = this.curNode.length;
          this.curText = '';
          if (this.curNode === this.rootNode) {
            return '';
          } else {
            this.curNode = this.curNode.previousSibling;
            return this.getPreviousClause();
          }
        }
      },
     
      getNextClause: function () {
        var text;
     
        if (!this.curNode) {
          return '';
        }
     
        if (this.headingTags.indexOf(this.curNode.nodeName) >= 0) { 
          this.foundNewHeading = true;
        }
     
        if (this.paragraphTags.indexOf(this.curNode.nodeName) >= 0) {
          this.foundNewParagraph = true;
        }
     
        if (this.curNode.nodeName === 'NOFRAMES' ||
            this.curNode.nodeName === '#comment' ||
            this.curNode.nodeName === 'NOSCRIPT' ||
            this.getStyle(this.curNode, 'display') === 'none' ||
            this.getStyle(this.curNode, 'visibility') === 'hidden') {
          text = ''; // this is meaningless only for passing JSLint
        } else if (this.curNode.nodeName === 'FRAME' ||
            this.curNode.nodeName === 'IFRAME') {
          this.curPos = 0;
          this.curText = '';
          try {
            if (this.curNode.contentDocument) {
              this.frameStack.push(this.curNode);
              this.curNode = this.curNode.contentDocument.body;
              return this.getNextClause();
            }
          } catch (e) {
            text = '';
          }
        } else if (!this.curNode.hasChildNodes()) {
          // text node, try to get text to speak
          if (this.curNode.nodeValue) {
            text = this.curNode.nodeValue.substr(this.curPos,
                this.curNode.nodeValue.length - this.curPos);
            if (text.length > 0) {
              text = this.getFirstClause(text);
              if (text.length > 0) {
                this.curPos = this.curNode.nodeValue.indexOf(text, this.curPos) + text.length;
                // this is it!
                this.curText = text;
                return text;
              }
            }
     
            do {
              this.curPos += 1;
            } while (this.curPos < this.curNode.nodeValue.length &&
                this.curNode.nodeValue.charAt(this.curPos).match(/\s/));
            if (this.curPos < this.curNode.nodeValue.length) {
              return this.getNextClause();
            }
          }
        } else {
          this.curPos = 0;
          this.curText = '';
          this.curNode = this.curNode.firstChild;
          return this.getNextClause();
        }
     
        if (this.curNode.nextSibling && this.curNode !== this.rootNode) {
          // turn to next sibling
          this.curNode = this.curNode.nextSibling;
          this.curPos = 0;
          this.curText = '';
          return this.getNextClause();
        } else {
          // turn to parent node's sibling
          this.curPos = 0;
          this.curText = '';
          do {
            if (!this.curNode.parentNode) {
              if (this.curNode.nodeName === '#document' &&
                  this.frameStack.length > 0) {
                this.curNode = this.frameStack.pop();
              } else {
                this.log('no parent');
                this.curNode = this.rootNode;
              }
            } else {
              this.curNode = this.curNode.parentNode;
            }
          } while (this.curNode !== this.rootNode && (!this.curNode.nextSibling));
     
          if (this.curNode === this.rootNode) {
            return '';
          } else {
            this.curNode = this.curNode.nextSibling;
            return this.getNextClause();
          }
        }
      },
     
      handleNavigationKeyUp: function (e) {
        // Usage: insert follwing code to <body>
        // onkeydown = 'if (typeof(WebSpeech) !== "undefined")
        //     { WebSpeech.handleNavigationKeyUp(event); }'
        // Return true if the key is handled
        // Key event reference: http://www.quirksmode.org/js/keys.html
        var code;
        if (window.event) { // IE
          code = e.keyCode;
        } else if (e.which) { // Netscape/Firefox/Opera
          code = e.which;
        }
     
        //this.log('key up: ' + code); 
        switch (code) {
        case 17:
          // CTRL (pause)
          this.isCtrlKeyDown = false;
          return false;
        case 16:
          // SHIFT (continue)
          this.isShiftKeyDown = false;
          return false;
        case 38:
          // ARROW-UP (previous sentence)
          if (this.state !== 'SPEAKHTML') {
            return false;
          }
          this.speakPreviousClause();
          return true;
        case 40:
          // ARROW-DOWN (next sentence)
          if (this.state !== 'SPEAKHTML') {
            return false;
          }
          this.speakNextClause();
          return true;
        case 72:
          // h/H (next heading)
          if (this.state !== 'SPEAKHTML') {
            return false;
          } 
          if (this.isShiftKeyDown) {
            // SHIFT + h/H
            this.speakPreviousHeading();
          } else {
            this.speakNextHeading();
          }
          return true;
        case 80:
          // p next paragraph
          if (this.state !== 'SPEAKHTML') {
            return false;
          }
          if (this.isShiftKeyDown) {
            // SHIFT + p/P
            this.speakPreviousParagraph();
          } else {
            this.speakNextParagraph();
          }
          return true;
     
          // next/previous page
     
          // TAB (next element)
          // SHIFT + TAB (previous element)
        default:
          //this.log('unhandled key up: ' + code);
          return false;
        }
      },
     
      handleNavigationKeyDown: function (e) {
        // Usage: insert follwing code to <body>
        // onkeydown = 'if (typeof(WebSpeech) !== "undefined")
        //     { WebSpeech.handleNavigationKeyDown(event); }'
        // Return true if the key is handled
        // Key event reference: http://www.quirksmode.org/js/keys.html
        var code;
        if (window.event) { // IE
          code = e.keyCode;
        } else if (e.which) { // Netscape/Firefox/Opera
          code = e.which;
        }
     
        //this.log('key down: ' + code); 
        switch (code) {
        case 17:
          // CTRL (pause)
          this.isCtrlKeyDown = true;
          if (this.isShiftKeyDown) {
            if (this.state === 'SPEAKHTML') {
              this.stopHtml();
            } else {
              this.speakHtml();
            }
          } else {
            this.pauseHtml();
          }
          return true;
        case 16:
          // SHIFT (continue)
          this.isShiftKeyDown = true;
          if (this.isCtrlKeyDown) {
            if (this.state === 'SPEAKHTML') {
              this.stopHtml();
            } else {
              this.speakHtml();
            }
          } else {
            this.resumeHtml();
          }
          return true;
     
        default:
          //this.log('unhandled key down: ' + code);
          return false;
        }
      },
     
      play: function (name) {
        if (!soundManager.supported()) {
          soundManager.onready(function () { WebSpeech.play(name); });
          return;
        }
        soundManager.play(name, 'sounds/' + name + '.mp3');
      },
     
      saveMp3: function (text) {
        window.open(this.getUrl('SAVEMP3', text));
      },
     
      saveOgg: function (text) {
        window.open(this.getUrl('SAVEOGG', text));
      },
     
      getPhonSymbols: function (text, callback) {
        OX.AJAST.call(this.getUrl('GETPHONSYMBOLS', text), 'callback', callback);
      },
     
      setVoice: function (voice) {
        if (!soundManager.supported()) {
          soundManager.onready(function () { WebSpeech.setVoice(voice); });
          return;
        }
        this.voice = voice;
      },
     
      getSpeedDelta: function () { return this.speedDelta; },
      setSpeedDelta: function (speedDelta) {
        if (!soundManager.supported()) {
          soundManager.onready(function () { WebSpeech.setSpeedDelta(speedDelta); });
          return;
        }
        if (speedDelta >= -50 && speedDelta <= 100) {
          this.speedDelta = speedDelta;
        }
        return this.speedDelta;
      },
     
      getPitchDelta: function () { return this.pitchDelta; },
      setPitchDelta: function (pitchDelta) {
        if (!soundManager.supported()) {
          soundManager.onready(function () { WebSpeech.setPitchDelta(pitchDelta); });
          return;
        }
        if (pitchDelta >= -100 && pitchDelta <= 100) {
          this.pitchDelta = pitchDelta;
        }
        return this.pitchDelta;
      },
     
      getVolumeDelta: function () { return this.volumeDelta; },
      setVolumeDelta: function (volumeDelta) {
        if (!soundManager.supported()) {
          soundManager.onready(function () { WebSpeech.setVolumeDelta(volumeDelta); });
          return;
        }
        if (volumeDelta >= -100 && volumeDelta <= 100) {
          this.volumeDelta = volumeDelta;
        }
        return this.volumeDelta;
      },
     
      pause: function () {
        if (this.curSpeech) {
          this.curSpeech.pause();
        }
      },
     
      pauseHtml: function () {
        if (this.curSpeech) {
          this.curSpeech.pause();
        }
      },
     
      resume: function () {
        if (this.curSpeech && this.curSpeech.paused) {
          this.curSpeech.resume();
        }
      },
     
      resumeHtml: function () {
        if (this.state === 'SPEAK_ONE_CLAUSE') {
          this.state = 'SPEAKHTML';
        }
     
        if (this.curSpeech && this.curSpeech.paused) {
          this.curSpeech.resume();
        } else {
          this.playNextSpeech();
        }
      },
     
      stop: function () {
        if (this.speechQueue.length > 0) {
          soundManager.stop(this.speechQueue[0]);
          while (this.speechQueue.length > 0) {
            this.speechQueue.pop();
          }
        }
      },
     
      stopHtml: function () {
        this.speechQueue = [];
        if (this.curSpeech) {
          this.curSpeech.stop();
        }
        this.state = 'NONE';
        this.disableHighLight();
        this.cacheCount = 0;
      }
    };
     
    // This file contains a simple Javascript broker that encapsulates 
    // the AJAST technique, allowing for cross-domain REST 
    // (REpresentatoinal State Transfer) calls.
    // 
    // Copyright (c) 2008 Håvard Stranden <havard.stranden@gmail.com>
    //
    // Permission is hereby granted, free of charge, to any person
    // obtaining a copy of this software and associated documentation
    // files (the "Software"), to deal in the Software without
    // restriction, including without limitation the rights to use,
    // copy, modify, merge, publish, distribute, sublicense, and/or sell
    // copies of the Software, and to permit persons to whom the
    // Software is furnished to do so, subject to the following
    // conditions:
    // 
    // The above copyright notice and this permission notice shall be
    // included in all copies or substantial portions of the Software.
    // 
    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    // OTHER DEALINGS IN THE SOFTWARE.
     
    if(typeof(OX) === 'undefined') OX = {};
    OX.AJAST = 
    {
      Broker : function(url, callbackparameter, optional_decode_json_response, optional_timeout_milliseconds, optional_default_params)
      {
        this.url = url;
        this.cb = callbackparameter;
        this.params = [];
        this.timeout = optional_timeout_milliseconds || 15000; // Timeout in milliseconds
        if(typeof(optional_default_params) !== 'undefined')
        {
          for(p in optional_default_params)
            this.params.push(p + '=' + encodeURIComponent(optional_default_params[p]));
        }
         
        this.jsonmode = optional_decode_json_response || false;
      },
       
      __callbacks__ : {},
       
      __callid__ : 1,
       
      call: function(url, callbackparameter, callbackfunction, optional_timeout, optional_decode_json_response)
      {
        var callbackid = 'callback' + OX.AJAST.__callid__;
         
        // Append callback parameter (this also implicitly avoids caching, since the callback id is different for each call)
        url += '&' + encodeURIComponent(callbackparameter) + '=' + encodeURIComponent('OX.AJAST.__callbacks__.' + callbackid);
           
        // Create script tag for the call
        var tag = OX.AJAST.createScriptTag(url);
        // Get the head of the document
        var head = document.getElementsByTagName('head').item(0);
         
           
        // Create a timeout function  
        var timedout = function()
        {
          if(OX.AJAST.__callbacks__[callbackid] !== 'undefined') // If the callback still exists...
          {
            // Replace original wrapped callback with a dummy that just deletes itself
            OX.AJAST.__callbacks__[callbackid] = function(){ delete OX.AJAST.__callbacks__[callbackid]; }; 
            // Signal that the call timed out
            callbackfunction(false); 
            // Remove the script tag (timed out)
            head.removeChild(tag); 
          }    
        };
         
        // Create timer for the timeout function
        var timer = setTimeout(timedout, optional_timeout || 15000);
           
        var decode_response = optional_decode_json_response || false;
         
        // Create the callback function          
        OX.AJAST.__callbacks__[callbackid] = function(data)
        {
          // Clear the timeout
          clearTimeout(timer);
           
          if(typeof(data) === 'undefined')
            callbackfunction(false); // Callback with nothing
          else
          {
            callbackfunction(true, decode_response ? eval(data) : data);
          }
          // Replace original callback with a dummy function 
          delete OX.AJAST.__callbacks__[callbackid];
          // Remove the script tag (finished)
          head.removeChild(tag);
        };
         
        // Inject the call
        head.appendChild(tag);
      },
       
      createScriptTag: function(url)
      {
        var s = document.createElement('script');
        s.setAttribute('type', 'text/javascript');
        s.setAttribute('id', 'oxajastcall' + OX.AJAST.Broker.__callid__++);
        s.setAttribute('src', url);
        return s;
      }
    };
     
    OX.AJAST.Broker.prototype.call = function(params, callback)
    {
      // Create arguments
      var args = [];
      for(p in params)
        args.push(p + '=' + encodeURIComponent(params[p]));
      for(p in this.params)
        args.push(this.params[p]);
      OX.AJAST.call(this.url + '?' + args.join('&'), this.cb, callback, this.timeout, this.jsonmode);
    };
     
    /**
    *
    *  MD5 (Message-Digest Algorithm)
    *  http://www.webtoolkit.info/
    *
    **/
      
    var MD5 = function (string) {
      
        function RotateLeft(lValue, iShiftBits) {
            return (lValue<<iShiftBits) | (lValue>>>(32-iShiftBits));
        }
      
        function AddUnsigned(lX,lY) {
            var lX4,lY4,lX8,lY8,lResult;
            lX8 = (lX & 0x80000000);
            lY8 = (lY & 0x80000000);
            lX4 = (lX & 0x40000000);
            lY4 = (lY & 0x40000000);
            lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF);
            if (lX4 & lY4) {
                return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
            }
            if (lX4 | lY4) {
                if (lResult & 0x40000000) {
                    return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
                } else {
                    return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
                }
            } else {
                return (lResult ^ lX8 ^ lY8);
            }
        }
      
        function F(x,y,z) { return (x & y) | ((~x) & z); }
        function G(x,y,z) { return (x & z) | (y & (~z)); }
        function H(x,y,z) { return (x ^ y ^ z); }
        function I(x,y,z) { return (y ^ (x | (~z))); }
      
        function FF(a,b,c,d,x,s,ac) {
            a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac));
            return AddUnsigned(RotateLeft(a, s), b);
        };
      
        function GG(a,b,c,d,x,s,ac) {
            a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac));
            return AddUnsigned(RotateLeft(a, s), b);
        };
      
        function HH(a,b,c,d,x,s,ac) {
            a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac));
            return AddUnsigned(RotateLeft(a, s), b);
        };
      
        function II(a,b,c,d,x,s,ac) {
            a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac));
            return AddUnsigned(RotateLeft(a, s), b);
        };
      
        function ConvertToWordArray(string) {
            var lWordCount;
            var lMessageLength = string.length;
            var lNumberOfWords_temp1=lMessageLength + 8;
            var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64;
            var lNumberOfWords = (lNumberOfWords_temp2+1)*16;
            var lWordArray=Array(lNumberOfWords-1);
            var lBytePosition = 0;
            var lByteCount = 0;
            while ( lByteCount < lMessageLength ) {
                lWordCount = (lByteCount-(lByteCount % 4))/4;
                lBytePosition = (lByteCount % 4)*8;
                lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<<lBytePosition));
                lByteCount++;
            }
            lWordCount = (lByteCount-(lByteCount % 4))/4;
            lBytePosition = (lByteCount % 4)*8;
            lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<<lBytePosition);
            lWordArray[lNumberOfWords-2] = lMessageLength<<3;
            lWordArray[lNumberOfWords-1] = lMessageLength>>>29;
            return lWordArray;
        };
      
        function WordToHex(lValue) {
            var WordToHexValue="",WordToHexValue_temp="",lByte,lCount;
            for (lCount = 0;lCount<=3;lCount++) {
                lByte = (lValue>>>(lCount*8)) & 255;
                WordToHexValue_temp = "0" + lByte.toString(16);
                WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2);
            }
            return WordToHexValue;
        };
      
        function Utf8Encode(string) {
            string = string.replace(/\r\n/g,"\n");
            var utftext = "";
      
            for (var n = 0; n < string.length; n++) {
      
                var c = string.charCodeAt(n);
      
                if (c < 128) {
                    utftext += String.fromCharCode(c);
                }
                else if((c > 127) && (c < 2048)) {
                    utftext += String.fromCharCode((c >> 6) | 192);
                    utftext += String.fromCharCode((c & 63) | 128);
                }
                else {
                    utftext += String.fromCharCode((c >> 12) | 224);
                    utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                    utftext += String.fromCharCode((c & 63) | 128);
                }
      
            }
      
            return utftext;
        };
      
        var x=Array();
        var k,AA,BB,CC,DD,a,b,c,d;
        var S11=7, S12=12, S13=17, S14=22;
        var S21=5, S22=9 , S23=14, S24=20;
        var S31=4, S32=11, S33=16, S34=23;
        var S41=6, S42=10, S43=15, S44=21;
      
        string = Utf8Encode(string);
      
        x = ConvertToWordArray(string);
      
        a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476;
      
        for (k=0;k<x.length;k+=16) {
            AA=a; BB=b; CC=c; DD=d;
            a=FF(a,b,c,d,x[k+0], S11,0xD76AA478);
            d=FF(d,a,b,c,x[k+1], S12,0xE8C7B756);
            c=FF(c,d,a,b,x[k+2], S13,0x242070DB);
            b=FF(b,c,d,a,x[k+3], S14,0xC1BDCEEE);
            a=FF(a,b,c,d,x[k+4], S11,0xF57C0FAF);
            d=FF(d,a,b,c,x[k+5], S12,0x4787C62A);
            c=FF(c,d,a,b,x[k+6], S13,0xA8304613);
            b=FF(b,c,d,a,x[k+7], S14,0xFD469501);
            a=FF(a,b,c,d,x[k+8], S11,0x698098D8);
            d=FF(d,a,b,c,x[k+9], S12,0x8B44F7AF);
            c=FF(c,d,a,b,x[k+10],S13,0xFFFF5BB1);
            b=FF(b,c,d,a,x[k+11],S14,0x895CD7BE);
            a=FF(a,b,c,d,x[k+12],S11,0x6B901122);
            d=FF(d,a,b,c,x[k+13],S12,0xFD987193);
            c=FF(c,d,a,b,x[k+14],S13,0xA679438E);
            b=FF(b,c,d,a,x[k+15],S14,0x49B40821);
            a=GG(a,b,c,d,x[k+1], S21,0xF61E2562);
            d=GG(d,a,b,c,x[k+6], S22,0xC040B340);
            c=GG(c,d,a,b,x[k+11],S23,0x265E5A51);
            b=GG(b,c,d,a,x[k+0], S24,0xE9B6C7AA);
            a=GG(a,b,c,d,x[k+5], S21,0xD62F105D);
            d=GG(d,a,b,c,x[k+10],S22,0x2441453);
            c=GG(c,d,a,b,x[k+15],S23,0xD8A1E681);
            b=GG(b,c,d,a,x[k+4], S24,0xE7D3FBC8);
            a=GG(a,b,c,d,x[k+9], S21,0x21E1CDE6);
            d=GG(d,a,b,c,x[k+14],S22,0xC33707D6);
            c=GG(c,d,a,b,x[k+3], S23,0xF4D50D87);
            b=GG(b,c,d,a,x[k+8], S24,0x455A14ED);
            a=GG(a,b,c,d,x[k+13],S21,0xA9E3E905);
            d=GG(d,a,b,c,x[k+2], S22,0xFCEFA3F8);
            c=GG(c,d,a,b,x[k+7], S23,0x676F02D9);
            b=GG(b,c,d,a,x[k+12],S24,0x8D2A4C8A);
            a=HH(a,b,c,d,x[k+5], S31,0xFFFA3942);
            d=HH(d,a,b,c,x[k+8], S32,0x8771F681);
            c=HH(c,d,a,b,x[k+11],S33,0x6D9D6122);
            b=HH(b,c,d,a,x[k+14],S34,0xFDE5380C);
            a=HH(a,b,c,d,x[k+1], S31,0xA4BEEA44);
            d=HH(d,a,b,c,x[k+4], S32,0x4BDECFA9);
            c=HH(c,d,a,b,x[k+7], S33,0xF6BB4B60);
            b=HH(b,c,d,a,x[k+10],S34,0xBEBFBC70);
            a=HH(a,b,c,d,x[k+13],S31,0x289B7EC6);
            d=HH(d,a,b,c,x[k+0], S32,0xEAA127FA);
            c=HH(c,d,a,b,x[k+3], S33,0xD4EF3085);
            b=HH(b,c,d,a,x[k+6], S34,0x4881D05);
            a=HH(a,b,c,d,x[k+9], S31,0xD9D4D039);
            d=HH(d,a,b,c,x[k+12],S32,0xE6DB99E5);
            c=HH(c,d,a,b,x[k+15],S33,0x1FA27CF8);
            b=HH(b,c,d,a,x[k+2], S34,0xC4AC5665);
            a=II(a,b,c,d,x[k+0], S41,0xF4292244);
            d=II(d,a,b,c,x[k+7], S42,0x432AFF97);
            c=II(c,d,a,b,x[k+14],S43,0xAB9423A7);
            b=II(b,c,d,a,x[k+5], S44,0xFC93A039);
            a=II(a,b,c,d,x[k+12],S41,0x655B59C3);
            d=II(d,a,b,c,x[k+3], S42,0x8F0CCC92);
            c=II(c,d,a,b,x[k+10],S43,0xFFEFF47D);
            b=II(b,c,d,a,x[k+1], S44,0x85845DD1);
            a=II(a,b,c,d,x[k+8], S41,0x6FA87E4F);
            d=II(d,a,b,c,x[k+15],S42,0xFE2CE6E0);
            c=II(c,d,a,b,x[k+6], S43,0xA3014314);
            b=II(b,c,d,a,x[k+13],S44,0x4E0811A1);
            a=II(a,b,c,d,x[k+4], S41,0xF7537E82);
            d=II(d,a,b,c,x[k+11],S42,0xBD3AF235);
            c=II(c,d,a,b,x[k+2], S43,0x2AD7D2BB);
            b=II(b,c,d,a,x[k+9], S44,0xEB86D391);
            a=AddUnsigned(a,AA);
            b=AddUnsigned(b,BB);
            c=AddUnsigned(c,CC);
            d=AddUnsigned(d,DD);
        }
      
        var temp = WordToHex(a)+WordToHex(b)+WordToHex(c)+WordToHex(d);
      
        return temp.toLowerCase();
    };
}
    /** @license
     *
     * SoundManager 2: JavaScript Sound for the Web
     * ----------------------------------------------
     * http://schillmania.com/projects/soundmanager2/
     *
     * Copyright (c) 2007, Scott Schiller. All rights reserved.
     * Code provided under the BSD License:
     * http://schillmania.com/projects/soundmanager2/license.txt
     *
     * V2.97a.20140901
     */