UNPKG

mmir-lib

Version:

MMIR (Mobile Multimodal Interaction and Relay) library

944 lines (792 loc) 28.2 kB
define(['mmirf/mediaManager', 'mmirf/configurationManager', 'mmirf/languageManager', 'mmirf/resources', 'mmirf/util/isArray', 'mmirf/logger'], function(mediaManager, config, lang, consts, isArray, Logger){ /** * @class WebAudioTextToSpeech * @memberOf mmir.env.media * @hideconstructor */ return { /** @memberOf mmir.env.media.WebAudioTextToSpeech */ initialize: function(callBack, ctxId, moduleConfig){ /** * @readonly * @inner * @default "audiotts" * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var _basePluginName = 'audiotts'; /** * Default implementation for WebAudioTTS: MARY TTS * @readonly * @inner * @default "ttsMary.js" * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var _defaultImplFile = 'ttsMary.js'; /** * When specifying the implementation file for an Audio Module Context * * i.e. * {"mod": "webAudioTextToSpeech.js", "ctx": CONTEXT} * * then plugin configuration will be check, if there is an entry for the ctx, * i.e. an implementation file name for entry ctx: * "webAudioTextToSpeech": { CONTEXT: IMPLEMENTATION_FILE_ENTRY }, * * Otherwise the plugin configuration's default entry will be used * "webAudioTextToSpeech": { CONTEXT: _defaultCtxName }, * * If no default entry exists, then {@link #_defaultImplFile} will be used as * implementation. * * @defaultValue "default" * @memberOf mmir.env.media.WebAudioTextToSpeech# * @private */ var _defaultCtxName = 'default'; //backwards compatibility (pre 3.?) if(!mediaManager._preparing){ mediaManager._preparing = function(name){_logger.warn(name +' is preparing - NOTE: this is a stub-function. Overwrite MediaManager._preparing for setting custom implementation.');}; } if(!mediaManager._ready){ mediaManager._ready = function(name){_logger.warn(name + ' is ready - NOTE: this is a stub-function. Overwrite MediaManager._ready for setting custom implementation.');}; } /** * Will be set/"overwritten" by specific implementation. * * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# * @see #initImpl */ var _pluginName; /** * Will be set when specific implementation is initialized. * * @type mmir.tools.Logger * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# * @see #initImpl */ var _logger = Logger.create();//<- set default logger for (will be replaced by specific logger after loading the implementation) /** * Will be set/"overwritten" by specific implementation. * * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var initSynthesizer;//TODO impl. re-initialization mechanism /** * Will be set/"overwritten" by specific implementation. * * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var destroySynthesizer; /** * Will be set/"overwritten" by specific implementation. * * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var createAudio; /** * Will be set/"overwritten" by specific implementation. * * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var getLanguages; /** * Will be set/"overwritten" by specific implementation. * * @protected * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var getVoices; /** @memberOf mmir.env.media.WebAudioTextToSpeech# * @private */ var volume = 1.0; /** @memberOf mmir.env.media.WebAudioTextToSpeech# * @private */ var _setVolume = function(val){ volume = val; for(var i=0,size=audioArray.length; i < size; ++i){ if(audioArray[i] && audioArray[i].setVolume){ audioArray[i].setVolume(val); } } }; /** @memberOf mmir.env.media.WebAudioTextToSpeech# * @private */ var _resetCallbacks = function(){ currentFailureCallback = null; onEndCallback = null; onReadyCallback = null; }; /////////////////////////// command-queue for queuing up TTS requests (instead of discarding when already busy) //////// /** * command-queue in case TTS is currently in use * -> if TTS invoked, but currently not ready: add to queue * -> after processing current TTS: process next on queue * * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var commandQueue = []; /** command-queue * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var addToCommandQueue = function(args){ //copy argument list: var len = args.length; var list = new Array(len); for(var i=len-1; i >= 0; --i){ list[i] = args[i]; } commandQueue.push(list); }; /** command-queue * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var processNextInCommandQueue = function(){ isReady = false; if(commandQueue.length > 0){ var args = commandQueue.shift(); isReady = true; _instance.textToSpeech.apply(_instance, args); } else { isReady = true; } }; /** command-queue * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var clearCommandQueue = function(){ commandQueue.splice(0); }; // ///////////////////////// END: command-queue ////////////////////////////////// /** * @function * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var onEndCallback= null; /** * @function * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var currentFailureCallback = null; /** * @function * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var onReadyCallback= null; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var isReady= true; /** internal field for single-sentence audio-object. * Used in {@link #ttsSingleSentence} * @type AudioImpl * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var ttsMedia = null; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var playIndex = 0; // /** @private // * @memberOf mmir.env.media.WebAudioTextToSpeech# */ // var firstSentence = true; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var loadIndex = 0; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var isLoading = false; /** * number of audio-objects that should be pre-fetched/buffered * when in sentence-mode. * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var bufferSize = 3; /** internal field for list of "sentence audio-object". * Used in {@link #ttsSentenceArray} * @type Array<AudioImpl> * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var audioArray = []; /** internal field for list of (text) sentences to be played. * Used in {@link #ttsSentenceArray} * @type Array<String> * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var sentenceArray = []; var callOptions = void(0); /** * In sentence mode: pause (in milli-seconds) between reading sentences. * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var pauseDuration = 500; /** * Placeholder for empty text / sentences: * no audio will be created for empty text/sentences, but using this placeholder-audio, * pause-durations etc will be applied (e.g. during sentence-mode). * * @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var EMPTY_SENTENCE = mediaManager.createEmptyAudio(); EMPTY_SENTENCE.type = 'empty'; EMPTY_SENTENCE.play = function(){ //simulate async playing via setTimeout setTimeout(function(){ _logger.debug("done playing EMPTY_SENTENCE"); //trigger playing the next entry: playNextAfterPause(); }, 10); //silence did start to play immediately, so return TRUE return true; }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var getLangParam = function(options, langSeparator, useLongLangCode){ return options && options.language? options.language : lang.getLanguageConfig(_pluginName, useLongLangCode? 'long' : 'language', langSeparator); }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var getVoiceParam = function(options){ //NOTE voice-options may be empty string -> need to check against undefined return options && typeof options.voice !== 'undefined'? options.voice : lang.getLanguageConfig(_pluginName, 'voice'); }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var playNextAfterPause = function(){ if(playIndex < audioArray.length - 1){ var pause = callOptions && typeof callOptions.pauseDuration === 'number'? callOptions.pauseDuration : pauseDuration; if(_logger.isd()) _logger.d("LongTTS play next in "+pause+ " ms... "); setTimeout(playNext, pause); } else { if(_logger.isd()) _logger.d("LongTTS played last audio, finishing up now... "); playNext(); } }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var playNext = function playNext(){ playIndex++; if (playIndex < audioArray.length){ if(audioArray[playIndex]){ ttsMedia=audioArray[playIndex]; if(_logger.isd()) _logger.d("LongTTS playing "+playIndex+ " '"+sentenceArray[playIndex]+ "'" + (!audioArray[playIndex].isEnabled()?' DISABLED':'')); audioArray[playIndex].setVolume(volume); var startedImmediatelely = audioArray[playIndex].play(); if(!startedImmediatelely){ if(_logger.isd()) _logger.d('LongTTS '+playIndex+': preparing...'); mediaManager._preparing(_pluginName); audioArray[playIndex].__notready = true; if(onReadyCallback){ onReadyCallback(false, audioArray[playIndex]); } } loadNext(); } else { if(_logger.isd()) _logger.d('LongTTS '+playIndex+': not ready, waiting until next is loaded ...'); mediaManager._preparing(_pluginName); if(onReadyCallback){ if(_logger.isd()) _logger.d('LongTTS '+playIndex+': audio not yet loading...'); onReadyCallback(false, audioArray[playIndex]); } // -> audio is not yet loaded... // request loading the next audio, and use playNext as onLoaded-callback: loadNext(playNext); } } else { if (onEndCallback){ onEndCallback(); _resetCallbacks(); } // isReady = true;//DISABLED -> EXPERIMENTAL: command-queue feature. //EXPERIMENTAL: command-queue feature. processNextInCommandQueue(); } }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var ttsSingleSentence = function(text, onEnd, failureCallback, onLoad, options){ try { isReady = false; mediaManager._preparing(_pluginName); ttsMedia = createAudio(text, options, function(){ // isReady = true;//DISABLED -> EXPERIMENTAL: command-queue feature. if(onEnd){ onEnd(); } ttsMedia.release(); //EXPERIMENTAL: command-queue feature. processNextInCommandQueue(); }, function(err){ // isReady = true;//DISABLED -> EXPERIMENTAL: command-queue feature. mediaManager._ready(_pluginName); if (failureCallback){ failureCallback(err); } else { _logger.error(err); } processNextInCommandQueue(); }, function(){ mediaManager._ready(_pluginName); if(onLoad){ onLoad(true, this); } }); ttsMedia.play(); } catch (err){ if (failureCallback){ failureCallback(err); } else { _logger.error(err); } processNextInCommandQueue(); } }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var ttsSentenceArray = function(sentences, onEnd, failureCallback, onInit, options){ { try { // firstSentence = true; //"clean up" texts in sentence array (ignore empty texts) var size = sentences.length; var theText = null; callOptions = options; sentenceArray= []; for(var i=0; i < size; ++i){ if(sentences[i] && sentences[i].length > 0){ theText = sentences[i].trim(); if(theText.length > 0){ sentenceArray.push(theText); } else { sentenceArray.push(EMPTY_SENTENCE); } } else { sentenceArray.push(EMPTY_SENTENCE); } } onEndCallback = onEnd; currentFailureCallback = failureCallback; onReadyCallback = onInit; playIndex = -1; loadIndex = -1; audioArray = new Array(sentences.length); isLoading = false; loadNext(onInit); } catch (err){ if (failureCallback){ failureCallback(err); _resetCallbacks(); } processNextInCommandQueue(); } } }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var loadNext = function loadNext(onInit){//TODO not onInit is currently only used for the very first sentence ... if (isLoading) return null; //FIXME need to handle case that loadingIndex is not within buffer-size ... if (((loadIndex-playIndex) <= bufferSize) && (loadIndex < (audioArray.length-1))){ isLoading = true; var currIndex = ++loadIndex; var currSentence = sentenceArray[currIndex]; if(_logger.isd()) _logger.d("LongTTS loading "+currIndex+ " "+currSentence); if(currSentence !== EMPTY_SENTENCE){ audioArray[currIndex] = createAudio(currSentence, callOptions, function onend(){ if(_logger.isd()) _logger.d("LongTTS done playing "+currIndex+ " '"+sentenceArray[currIndex]+"'"); if(audioArray[currIndex]){//GUARD: audio object may have been removed by cancelSpeech(), and this listener triggered async audioArray[currIndex].release(); } playNextAfterPause(); }, function onerror(err){ //TODO currently, all pending sentences are aborted in case of an error // -> should we try the next sentence instead? if (currentFailureCallback){ currentFailureCallback(err); _resetCallbacks(); } processNextInCommandQueue(); }, function oninit(){ if(_logger.isd()) _logger.d("LongTTS done loading "+currIndex+ " "+sentenceArray[currIndex]+ (!this.isEnabled()?' DISABLED':'')); isLoading = false; loadNext(); if(onInit){ if(_logger.isd()) _logger.d('LongTTS: invoking onInit now.'); mediaManager._ready(_pluginName); onInit(true, this); } else if(this.__notready && onReadyCallback){ if(_logger.isd()) _logger.d('LongTTS: ready again.'); mediaManager._ready(_pluginName); onReadyCallback(true, this); } } ); } else { audioArray[currIndex] = EMPTY_SENTENCE; if(_logger.isd()) _logger.d("LongTTS done loading "+currIndex+ " EMPTY_SENTENCE"); isLoading = false; loadNext(); if(onInit){ onInit(true, EMPTY_SENTENCE); } } if (currIndex === 0){ playNext(); } loadNext(); } }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var initImpl = function(implFactory){ var impl = implFactory(/* pass-in the default logger: */ Logger.create() ); var pluginName = impl.getPluginName(); _pluginName = pluginName; _logger = Logger.create(_pluginName + (ctxId? ':'+ctxId : '')); createAudio = impl.getCreateAudioFunc(); // optional hooks: getLanguages = impl.getLanguageListFunc? impl.getLanguageListFunc() : _logger.warn('initialization error: missing function '+pluginName+'.getLanguageListFunc()!') && void(0); getVoices = impl.getVoiceListFunc? impl.getVoiceListFunc() : _logger.warn('initialization error: missing function '+pluginName+'.getVoiceListFunc()!') && void(0); initSynthesizer = impl.getInitFunc? impl.getInitFunc() : _logger.debug('no getInitFunc() function: will not be able to re-initialize '+pluginName) && void(0); destroySynthesizer = impl.getDestroyFunc? impl.getDestroyFunc() : _logger.debug('no getDestroyFunc() function: will not be able to destroy '+pluginName) && void(0); //optional: set default implementations, if imp. "requests" it by specifying a setter function if(impl.setLogger){ //set logger impl.setLogger(_logger); } if(impl.setLangParamFunc){ //set default impl impl.setLangParamFunc(getLangParam); } if(impl.setVoiceParamFunc){ //set default impl impl.setVoiceParamFunc(getVoiceParam); } }; /** @private * @memberOf mmir.env.media.WebAudioTextToSpeech# */ var _instance = { /** * @deprecated use {@link #tts} instead * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype */ textToSpeech: function(){ return mediaManager.perform(ctxId, 'tts', arguments); }, /** * @copydoc mmir.MediaManager#tts * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * @see mmir.MediaManager#tts * @copydoc mmir.MediaManager#tts */ tts: function(options, successCallback, failureCallback, onReadyCallback, options2){//NOTE params onReadyCallback and options2 are deprecated, use (first) param options instead var errMsg; if (!isReady) { addToCommandQueue(arguments); return; } isReady = false; //convert first argument to options-object, if necessary if(typeof options === 'string' || isArray(options)){ options = {text: options}; } if(successCallback){ options.success = successCallback; } if(failureCallback){ options.error = failureCallback; } if(onReadyCallback){ options.ready = onReadyCallback; } //backwards-compatibility: copy old-API options-object over, if necessary if(options2){ for(var p in options2){ if(options2.hasOwnProperty(p)) options[p] = options2[p]; } } var text = options.text; var isMultiple = isArray(text); if(typeof text !== 'string' && !isArray(text)){ text = text? text.toString() : '' + text; options.text = text; } if(text.length === 0){ isReady = true; errMsg = "Aborted TTS: no text supplied (string has length 0)"; if(failureCallback){ failureCallback(errMsg); } else { _logger.error(errMsg); } processNextInCommandQueue(); return;/////////////////////////////////// EARLY EXIT ///////////////////////////// } if(!isMultiple){ ttsSingleSentence(text, options.success, options.error, options.ready, options); } else { ttsSentenceArray(text, options.success, options.error, options.ready, options); } }, /** * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * @see mmir.MediaManager#cancelSpeech * @copydoc mmir.MediaManager#cancelSpeech */ cancelSpeech: function(successCallback, failureCallback){ _logger.debug('cancel tts...'); try { //immediately disable onReadyCallback / ready callbacks: onReadyCallback = null; //NOTE 1: use command-queue in case TTS is currently in use -> empty queue // TODO should queue stay left intact? i.e. only current TTS canceled ...? //NOTE 2: if command-queue would not be cleared: need to take care of delayed audio-objects // where the audio-object is not created immediately but delayed (e.g. via getWAVAsAudio(..) // as in nuanceHttpTextToSpeech), e.g. by using additional canceled-flag? clearCommandQueue(); //prevent further loading: loadIndex = audioArray.length; //disable playing for sentence-modus audioArray.forEach(function (audio){ if (audio) { audio.disable(); audio.release(); } }); //stop currently playing if (!isReady){ ttsMedia.disable(); } if(onEndCallback){ onEndCallback(); onEndCallback = null; } isReady = true; if(successCallback) successCallback(); } catch (e){ isReady = true; if (failureCallback) failureCallback(e); } _resetCallbacks(); }, /** * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * @see mmir.MediaManager#setTextToSpeechVolume * @copydoc mmir.MediaManager#setTextToSpeechVolume */ setTextToSpeechVolume: function(newValue){ _setVolume(newValue); }, /** * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * @see mmir.MediaManager#getSpeechLanguages * @copydoc mmir.MediaManager#getSpeechLanguages */ getSpeechLanguages: function(successCallback, failureCallback){ if(getLanguages){ getLanguages(successCallback, failureCallback); } else { var msg = 'plugin '+_pluginName+' does not support getSpeechLanguages()'; if(failureCallback){ failureCallback(msg); } else { _logger.error(msg); } } }, /** * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * @see mmir.MediaManager#getVoices * @copydoc mmir.MediaManager#getVoices */ getVoices: function(options, successCallback, failureCallback){ if(typeof options === 'function'){ failureCallback = successCallback; successCallback = options; options = void(0); } else if(options && typeof options === 'string'){ options = {language: options}; } if(getVoices){ getVoices(options, successCallback, failureCallback); } else { var msg = 'plugin '+_pluginName+' does not support getVoices()'; if(failureCallback){ failureCallback(msg); } else { _logger.error(msg); } } }, /** * Destroy the speech synthesizer instance and free up system resources. * * NOTE: may not be supported by all synthesizer implementations * (e.g. if the impl. does not block system resources etc) * * IMPORTANT: pluins that support destroySpeech() should also support initializeSpeech(). * * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * * @param {Function} [successCallback] callback in case of success: <pre>successCallback(didDestroy: boolean)</pre> * in case, the plugin does not support destroySpeech(), * <code>successCallback(false)</code> will be invoked * @param {Function} [failureCallback] callback that will be invoked in case of an error: <pre>failureCallback(error)</pre> * * @see #initializeSpeech */ destroySpeech: function(successCallback, failureCallback){ if(destroySynthesizer){ destroySynthesizer(function(err){ if(err){ if(failureCallback){ failureCallback(err); } else { _logger.error('audiotts: failed to destroy synthesizer instance: '+err, err); } } else if(successCallback) { successCallback(true); } }); } else { if(successCallback){ setTimeout(function() { successCallback(false); }, 0); } else if(_logger.isd()) { _logger.log('audiotts: plugin does not support destroying synthesizer instance: ignoring destroySpeech()'); } } }, /** * Re-initialize the speech synthesizer instance: * should be called after invoking <code>destroySpeech()<code> (and its success-callback returned <code>true</code>) * before continuing to use the synthesizer instance. * * NOTE: may not be supported by all synthesizer implementations * * IMPORTANT: pluins that support initializeSpeech() should also support destroySpeech(). * * @public * @memberOf mmir.env.media.WebAudioTextToSpeech.prototype * * @param {Function} [successCallback] callback in case of success: <pre>successCallback(didDestroy: boolean)</pre> * in case, the plugin does not support initializeSpeech(), * <code>successCallback(false)</code> will be invoked * @param {Function} [failureCallback] callback that will be invoked in case of an error: <pre>failureCallback(error)</pre> * * @see #destroySpeech */ initializeSpeech: function(successCallback, failureCallback){ if(initSynthesizer){ initSynthesizer(function(err){ if(err){ if(failureCallback){ failureCallback(err); } else { _logger.error('audiotts: failed to re-initialized synthesizer instance: '+err, err); } } else if(successCallback) { successCallback(true); } }); } else { if(successCallback){ setTimeout(function() { successCallback(false); }, 0); } else if(_logger.isd()) { _logger.log('audiotts: plugin does not support destroying synthesizer instance: ignoring destroySpeech()'); } } } };//END: _instance = { ... //load specific implementation of WebAudio TTS: var implFile, configPath = [_basePluginName, ctxId]; if(moduleConfig){ //if there config setting -> use as implementation-filename: implFile = moduleConfig; } else if(ctxId){ //if plugin was loaded into a specific context, check, if there is a configuration value for this context) implFile = config.get(configPath); } if(!implFile){ //use default configuration path configPath[1] = _defaultCtxName; implFile = config.get(configPath); } if(!implFile){ //if no configuration: use default impl. implFile = _defaultImplFile; } else if((typeof WEBPACK_BUILD === 'undefined' || !WEBPACK_BUILD) && !/\.js$/i.test(implFile)){ implFile += '.js'; } var rePluginId = /^mmir-plugin-[^/]+$/; var implPath = (rePluginId.test(implFile)? '' : consts.getMediaPluginPath()) + implFile; var processLoaded = function success(theNewWebAudioTtsImpl){ //initialize implementation: try { initImpl(theNewWebAudioTtsImpl); } catch(err){ handleError(err); return; //////////////// EARLY EXIT //////////////////// } //invoke the passed-in initializer-callback and export the public functions: callBack(_instance); }; var handleError = function error(err){ _logger.error('audiotts: failed to load media plugin (file) '+implPath+': '+err, err); //invoke the passed-in initializer-callback and supply list of non-functional functions: callBack({}, { mod: implPath, disabled: ['tts', 'cancelSpeech', 'setTextToSpeechVolume', 'getSpeechLanguages', 'getVoices', 'destroySpeech', 'initializeSpeech'], message: err }); }; if(typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD){ implFile = implFile.replace(/\.js$/i, ''); try{ var dep = rePluginId.test(implFile)? __webpack_require__(implFile) : require('./'+implFile); processLoaded(dep); } catch(err){ handleError(err); } } else { require([consts.getMediaPluginPath() + implPath], processLoaded, function(_err){ //try filePath as module ID instead: var moduleId = implFile.replace(/\.js$/i, ''); if(_logger.isd()) _logger.debug('audiotts: failed loading plugin from file '+implPath+', trying module ID ' + moduleId); require([moduleId], processLoaded, handleError); }); } } }; });//END define