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