1 /* 2 * Copyright (C) 2012-2013 DFKI GmbH 3 * Deutsches Forschungszentrum fuer Kuenstliche Intelligenz 4 * German Research Center for Artificial Intelligence 5 * http://www.dfki.de 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a 8 * copy of this software and associated documentation files (the 9 * "Software"), to deal in the Software without restriction, including 10 * without limitation the rights to use, copy, modify, merge, publish, 11 * distribute, sublicense, and/or sell copies of the Software, and to 12 * permit persons to whom the Software is furnished to do so, subject to 13 * the following conditions: 14 * 15 * The above copyright notice and this permission notice shall be included 16 * in all copies or substantial portions of the Software. 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 19 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 */ 26 27 28 newMediaPlugin = { 29 /** @memberOf MaryTextToSpeech# */ 30 initialize: function(callBack, mediaManager){ 31 32 /** @memberOf MaryTextToSpeech# */ 33 var _pluginName = 'maryTextToSpeech'; 34 35 /** 36 * @type mmir.LanguageManager 37 * @memberOf MaryTextToSpeech# 38 */ 39 var languageManager = require('languageManager'); 40 /** 41 * @type mmir.ConfigurationManager 42 * @memberOf MaryTextToSpeech# 43 */ 44 var configurationManager = require('configurationManager'); 45 /** 46 * @type mmir.CommonUtils 47 * @memberOf MaryTextToSpeech# 48 */ 49 var commonUtils = require('commonUtils'); 50 51 /** 52 * separator char for language- / country-code (specific to TTS service) 53 * 54 * @memberOf MaryTextToSpeech# 55 */ 56 var _langSeparator = void(0); 57 58 /** @memberOf MaryTextToSpeech# */ 59 var volume = 1.0; 60 61 /** @memberOf MaryTextToSpeech# */ 62 var _setVolume = function(val){ 63 volume = val; 64 65 for(var i=0,size=audioArray.length; i < size; ++i){ 66 if(audioArray[i] && audioArray[i].setVolume){ 67 audioArray[i].setVolume(val); 68 } 69 } 70 }; 71 72 /////////////////////////// EXPERIMENTAL: command-queue for queuing up TTS requests (instead of discarding when already busy) //////// 73 /** 74 * EXPERIMENTAL: command-queue in case TTS is currently in use 75 * -> if TTS invoked, but currently not ready: add to queue 76 * -> after processing current TTS: process next on queue 77 * 78 * @memberOf MaryTextToSpeech# 79 */ 80 var commandQueue = []; 81 /** EXPERIMENTAL: command-queue 82 * @memberOf MaryTextToSpeech# */ 83 var addToCommandQueue = function(args){ 84 85 //copy argument list: 86 var len = args.length; 87 var list = new Array(len); 88 for(var i=len-1; i >= 0; --i){ 89 list[i] = args[i]; 90 } 91 92 commandQueue.push(list); 93 }; 94 /** EXPERIMENTAL: command-queue 95 * @memberOf MaryTextToSpeech# */ 96 var processNextInCommandQueue = function(){ 97 98 isReady = false; 99 if(commandQueue.length > 0){ 100 var args = commandQueue.shift(); 101 isReady = true; 102 _instance.textToSpeech.apply(_instance, args); 103 } 104 else { 105 isReady = true; 106 } 107 108 }; 109 /** EXPERIMENTAL: command-queue 110 * @memberOf MaryTextToSpeech# */ 111 var clearCommandQueue = function(args){ 112 commandQueue.splice(0, commandQueue.length); 113 }; 114 // ///////////////////////// END: command-queue ////////////////////////////////// 115 116 /** 117 * @function 118 * @memberOf MaryTextToSpeech# */ 119 var onEndCallBack= null; 120 /** 121 * @function 122 * @memberOf MaryTextToSpeech# */ 123 var currentFailureCallBack = null; 124 /** @memberOf MaryTextToSpeech# */ 125 var isReady= true; 126 /** internal field for single-sentence audio-object. 127 * Used in {@link #ttsSingleSentence} 128 * @type AudioImpl 129 * @memberOf MaryTextToSpeech# */ 130 var ttsMedia = null; 131 /** @memberOf MaryTextToSpeech# */ 132 var playIndex = 0; 133 /** @memberOf MaryTextToSpeech# */ 134 var firstSentence = true; 135 /** @memberOf MaryTextToSpeech# */ 136 var loadIndex = 0; 137 /** @memberOf MaryTextToSpeech# */ 138 var isLoading = false; 139 /** 140 * number of audio-objects that should be pre-fetched/buffered 141 * when in sentence-mode. 142 * @memberOf MaryTextToSpeech# 143 */ 144 var bufferSize = 3; 145 /** internal field for list of "sentence audio-object". 146 * Used in {@link #ttsSentenceArray} 147 * @type Array<AudioImpl> 148 * @memberOf MaryTextToSpeech# 149 */ 150 var audioArray = []; 151 /** internal field for list of (text) sentences to be played. 152 * Used in {@link #ttsSentenceArray} 153 * @type Array<String> 154 * @memberOf MaryTextToSpeech# 155 */ 156 var sentenceArray = []; 157 158 var callOptions = void(0); 159 160 /** 161 * In sentence mode: pause (in milli-seconds) between reading sentences. 162 * @memberOf MaryTextToSpeech# 163 */ 164 var pauseDuration = 500; 165 166 /** 167 * Placeholder for empty text / sentences: 168 * no audio will be created for empty text/sentences, but using this placeholder-audio, 169 * pause-durations etc will be applied (e.g. during sentence-mode). 170 * 171 * @memberOf MaryTextToSpeech# 172 */ 173 var EMPTY_SENTENCE = mediaManager.createEmptyAudio(); 174 EMPTY_SENTENCE.type = 'empty'; 175 EMPTY_SENTENCE.play = function(){ 176 177 //simulate async playing via setTimeout 178 setTimeout(function(){ 179 180 console.log("done playing EMPTY_SENTENCE"); 181 console.log("LongTTS play next in "+pauseDuration+ " ms... "); 182 183 //trigger playing the next entry (after the set pause-duration) 184 setTimeout(playNext, pauseDuration); 185 186 }, 10); 187 188 }; 189 190 /** 191 * HELPER for splitting a single String into "sentences", i.e. Array of Strings 192 * @param {String} text 193 * the input String 194 * @returns {Array<String>} a list of strings 195 * @memberOf MaryTextToSpeech# 196 */ 197 var defaultSplitter = function(text){ 198 text = text.replace(/\.\s|\?\s|\!\s/g,"#"); 199 return text.split("#"); 200 }; 201 /** @memberOf MaryTextToSpeech# */ 202 var generateTTSURL = function(text, options){ 203 204 text = encodeURIComponent(text); 205 206 var lang = getLangParam(options); 207 208 var voice = getVoiceParam(options); 209 var voiceParamStr = voice? '&VOICE='+voice : ''; 210 211 return configurationManager.get([_pluginName, "serverBasePath"])+'process?INPUT_TYPE=TEXT&OUTPUT_TYPE=AUDIO&INPUT_TEXT=' + text + '&LOCALE='+lang + voiceParamStr + '&AUDIO=WAVE_FILE'; 212 }; 213 214 /** @memberOf MaryTextToSpeech# */ 215 var getLangParam = function(options){ 216 return options && options.language? options.language : languageManager.getLanguageConfig(_pluginName, 'language', _langSeparator); 217 }; 218 219 /** @memberOf MaryTextToSpeech# */ 220 var getVoiceParam = function(options){ 221 //NOTE voice-options may be empty string -> need to check against undefined 222 return options && typeof options.voice !== 'undefined'? options.voice : languageManager.getLanguageConfig(_pluginName, 'voice'); 223 }; 224 225 /** @memberOf MaryTextToSpeech# */ 226 var playNext = function playNext(){ 227 228 playIndex++; 229 if (playIndex < audioArray.length){ 230 231 if(audioArray[playIndex]){ 232 233 ttsMedia=audioArray[playIndex]; 234 235 console.log("LongTTS playing "+playIndex+ " '"+sentenceArray[playIndex]+ "'" + (!audioArray[playIndex].isEnabled()?' DISABLED':''));//FIXME debug 236 237 audioArray[playIndex].setVolume(volume); 238 audioArray[playIndex].play(); 239 loadNext(); 240 } 241 else { 242 // -> audio is not yet loaded... 243 // request loading the next audio, and use playNext as onLoaded-callback: 244 loadNext(playNext); 245 } 246 } 247 else { 248 if (onEndCallBack){ 249 onEndCallBack(); 250 onEndCallBack = null; 251 } 252 // isReady = true;//DISABLED -> EXPERIMENTAL: command-queue feature. 253 254 //EXPERIMENTAL: command-queue feature. 255 processNextInCommandQueue(); 256 } 257 }; 258 259 /** @memberOf MaryTextToSpeech# */ 260 var ttsSingleSentence = function(text, onEnd, failureCallBack, onLoad, options){ 261 262 try { 263 isReady = false; 264 ttsMedia = createAudio(text, options, 265 function(){ 266 // isReady = true;//DISABLED -> EXPERIMENTAL: command-queue feature. 267 if(onEnd){ 268 onEnd(); 269 }; 270 //EXPERIMENTAL: command-queue feature. 271 processNextInCommandQueue(); 272 }, 273 function(){ 274 // isReady = true;//DISABLED -> EXPERIMENTAL: command-queue feature. 275 if (failureCallBack){ 276 failureCallBack(); 277 }; 278 279 //EXPERIMENTAL: command-queue feature. 280 processNextInCommandQueue(); 281 }, 282 function(){ 283 if(onLoad){ 284 onLoad(); 285 }; 286 }); 287 ttsMedia.play(); 288 } catch (e){ 289 // isReady=true;//DISABLED -> EXPERIMENTAL: command-queue feature. 290 console.log('error!'+e); 291 if (failureCallBack){ 292 failureCallBack(); 293 } 294 295 //EXPERIMENTAL: command-queue feature. 296 processNextInCommandQueue(); 297 } 298 299 }; 300 301 /** @memberOf MaryTextToSpeech# */ 302 var ttsSentenceArray = function(sentences, onEnd, failureCallBack, onInit, options){ 303 { 304 try { 305 firstSentence = true; 306 307 //"clean up" texts in sentence array (ignore empty texts) 308 var size = sentences.length; 309 var theText = null; 310 311 callOptions = options; 312 sentenceArray= []; 313 for(var i=0; i < size; ++i){ 314 if(sentences[i] && sentences[i].length > 0){ 315 theText = sentences[i].trim(); 316 if(theText.length > 0){ 317 sentenceArray.push(theText); 318 } 319 else { 320 sentenceArray.push(EMPTY_SENTENCE); 321 } 322 } 323 else { 324 sentenceArray.push(EMPTY_SENTENCE); 325 } 326 } 327 328 onEndCallBack = onEnd; 329 currentFailureCallBack = failureCallBack; 330 playIndex = -1; 331 loadIndex = -1; 332 audioArray = new Array(sentences.length); 333 isLoading = false; 334 loadNext(onInit); 335 } catch (e){ 336 // isReady=true;//DISABLED -> EXPERIMENTAL: command-queue feature. 337 console.log('error! '+e); 338 if (failureCallBack){ 339 failureCallBack(); 340 } 341 342 //EXPERIMENTAL: command-queue feature. 343 processNextInCommandQueue(); 344 } 345 } 346 }; 347 348 /** @memberOf MaryTextToSpeech# */ 349 var loadNext = function loadNext(onInit){//TODO not onInit is currently only used for the very first sentence ... 350 351 if (isLoading) return null; 352 353 //FIXME need to handle case that loadingIndex is not within buffer-size ... 354 if (((loadIndex-playIndex)<= bufferSize) && (loadIndex<(audioArray.length-1))){ 355 isLoading = true; 356 var currIndex = ++loadIndex; 357 var currSentence = sentenceArray[currIndex]; 358 console.log("LongTTS loading "+currIndex+ " "+currSentence); 359 if(currSentence !== EMPTY_SENTENCE){ 360 361 audioArray[currIndex] = createAudio(currSentence, callOptions, 362 363 function onend(){ 364 console.log("LongTTS done playing "+currIndex+ " "+sentenceArray[currIndex]); 365 audioArray[currIndex].release(); 366 367 368 console.log("LongTTS play next in "+pauseDuration+ " ms... "); 369 setTimeout(playNext, pauseDuration); 370 371 //TODO only invoke this, if previously the test for (loadIndex-playIndex)<= bufferSize) failed ... 372 //loadNext(); 373 }, 374 375 function onerror(){ 376 //TODO currently, all pending sentences are aborted in case of an error 377 // -> should we try the next sentence instead? 378 379 // isReady = true;//DISABLE -> EXPERIMENTAL: command-queue feature. 380 if (currentFailureCallBack){ 381 currentFailureCallBack(); 382 }; 383 384 //EXPERIMENTAL: command-queue feature. 385 processNextInCommandQueue(); 386 }, 387 388 function oninit(){ 389 console.log("LongTTS done loading "+currIndex+ " "+sentenceArray[currIndex]+ (!this.isEnabled()?' DISABLED':'')); 390 isLoading = false; 391 loadNext(); 392 393 if(onInit){ 394 onInit(); 395 } 396 } 397 ); 398 } 399 else { 400 401 audioArray[currIndex] = EMPTY_SENTENCE; 402 403 console.log("LongTTS done loading "+currIndex+ " EMPTY_SENTENCE"); 404 isLoading = false; 405 loadNext(); 406 407 if(onInit){ 408 onInit(); 409 } 410 } 411 412 if (currIndex==0){ 413 playNext(); 414 } 415 416 loadNext(); 417 } 418 }; 419 420 /** @memberOf MaryTextToSpeech# */ 421 var createAudio = function(sentence, options, onend, onerror, oninit){ 422 423 return mediaManager.getURLAsAudio( 424 generateTTSURL(sentence, callOptions), 425 onend, onerror, oninit 426 ); 427 428 }; 429 430 /** @memberOf MaryTextToSpeech# */ 431 var _instance = { 432 /** 433 * @public 434 * @memberOf MaryTextToSpeech.prototype 435 * @see mmir.MediaManager#textToSpeech 436 */ 437 textToSpeech: function(parameter, successCallback, failureCallback, onInit, options){ 438 var errMsg; 439 if (!isReady) { 440 441 //EXPERIMENTAL: use command-queue in case TTS is currently in use. 442 addToCommandQueue(arguments); 443 return; 444 445 //EXPERIMENTAL: command-queue feature. 446 // -> DISABLED error case (not needed anymore, if queuing TTS requests...) 447 // errMsg = "TTS is already used at the moment."; 448 // if(failureCallback){ 449 // failureCallback(errMsg); 450 // } 451 // else { 452 // console.error(errMsg); 453 // } 454 // return; 455 } 456 isReady = false; 457 458 var text; 459 var isMultiple = false; 460 if (typeof parameter === 'object'){ 461 462 //TODO allow setting custom pause-duration, something like: (NOTE would need to reset pause in case of non-object arg too!) 463 // if (parameter.pauseDuration!== null && parameter.pauseDuration>=0){ 464 // pauseDuration = parameter.pauseDuration; 465 // console.log("PauseDuration: "+pauseDuration); 466 // } else { 467 // var configPause = configurationManager.get('pauseDurationBetweenSentences'); 468 // if (configPause) { 469 // pauseDuration = configPause; 470 // } 471 // else{ 472 // pauseDuration = 500; 473 // } 474 // } 475 476 if (parameter.text && commonUtils.isArray(parameter.text)){ 477 if (parameter.forceSingleSentence){ 478 text = commonUtils.concatArray(parameter.text); 479 } else { 480 text = parameter.text; 481 } 482 } 483 484 //if text is string: apply splitting, if requested: 485 if (typeof parameter.text === 'string'){ 486 if (parameter.split || parameter.splitter){ 487 var splitter = parameter.splitter || defaultSplitter; 488 text = splitter(parameter.text); 489 } else { 490 text = parameter.text; 491 } 492 } 493 494 if(!text){ 495 text = parameter; 496 } 497 498 if(!options){ 499 options = parameter; 500 } 501 //TODO else: merge parameter into options 502 503 } else { 504 text = parameter; 505 } 506 507 if(text && commonUtils.isArray(text)){ 508 isMultiple = true; 509 } else if (typeof text !== 'string'){ 510 text = typeof text !== 'undefined' && text !== null? text.toString() : '' + text; 511 } 512 513 if(text.length === 0){ 514 isReady = true; 515 errMsg = "Aborted TTS: no text supplied (string has length 0)"; 516 if(failureCallback){ 517 failureCallback(errMsg); 518 } 519 else { 520 console.error(errMsg); 521 } 522 523 //EXPERIMENTAL: command-queue feature. 524 processNextInCommandQueue(); 525 526 return;/////////////////////////////////// EARLY EXIT ///////////////////////////// 527 } 528 529 if(!isMultiple){ 530 531 ttsSingleSentence(text, successCallback, failureCallback, onInit, options); 532 533 } else { 534 535 ttsSentenceArray(text, successCallback, failureCallback, onInit, options); 536 } 537 }, 538 /** 539 * @public 540 * @memberOf MaryTextToSpeech.prototype 541 * @see mmir.MediaManager#cancelSpeech 542 */ 543 cancelSpeech: function(successCallBack, failureCallBack){ 544 console.debug('cancel tts...'); 545 try { 546 547 548 //EXPERIMENTAL: use command-queue in case TTS is currently in use -> empty queue 549 // TODO should queue stay left intact? i.e. only current TTS canceled ...? 550 //NOTE: if command-queue would not be cleared: need to take care of delayed audio-objects 551 // where the audio-object is not created immediately but delayed (e.g. via getWAVAsAudio(..) 552 // as in nuanceHttpTextToSpeech), e.g. by using additional canceled-flag? 553 clearCommandQueue(); 554 555 //prevent further loading: 556 loadIndex = audioArray.length; 557 558 //disable playing for sentence-modus 559 audioArray.forEach(function (audio){ 560 if (audio) { 561 audio.disable(); 562 audio.release(); 563 } 564 }); 565 566 //stop currently playing 567 if (!isReady){ 568 ttsMedia.disable(); 569 } 570 571 if(onEndCallBack){ 572 onEndCallBack(); 573 onEndCallBack = null; 574 } 575 576 isReady = true; 577 successCallBack(); 578 }catch (e){ 579 isReady = true; 580 if (failureCallBack) 581 failureCallBack(); 582 } 583 }, 584 /** 585 * @public 586 * @memberOf MaryTextToSpeech.prototype 587 * @see mmir.MediaManager#setTextToSpeechVolume 588 */ 589 setTextToSpeechVolume: function(newValue){ 590 _setVolume(newValue); 591 } 592 };//END: _instance = { ... 593 594 //invoke the passed-in initializer-callback and export the public functions: 595 callBack(_instance); 596 } 597 }; 598