package com.crosstales.RTVoice; //region Imports import android.annotation.TargetApi; import android.content.Context; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.speech.tts.TextToSpeech; import android.speech.tts.UtteranceProgressListener; import android.speech.tts.Voice; import android.util.Log; import java.io.File; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; //endregion /** * Acts as a handler for all TTS functions called by RT-Voice on Android. *

* Copyright 2016-2020 www.crosstales.com */ public class RTVoiceAndroidBridge { //region Variables public static final String VERSION = "2020.4.7"; //Context to instantiate TTS engine private static Context appContext; //TTS object private static TextToSpeech tts; //TTS engine is initialized private static boolean initialized; //TTS engine is currently busy private static boolean working = false; //pathname of the generated WAV file private static String outputFile; // Volume for native speaking private static float nativeVolume = 1f; // Set of all available Locales (SDK < 21) private static Set locales; // Tag for the logs private static final String TAG = "RTVoiceAndroidBridge"; private static final boolean DEBUG = false; //Change to enable debug logs //endregion //region Constructor /** * Constructor for the RTVoiceAndroidBridge class. * The appContext must contain the application context so we can initialize the TTS engine. * * @param appContext Application context of the Unity application */ public RTVoiceAndroidBridge(Object appContext) { RTVoiceAndroidBridge.initialized = false; RTVoiceAndroidBridge.appContext = (Context) appContext; if (DEBUG) Log.d(TAG, "Constructor called!"); //tts = createTTS(); } //endregion //region Public Methods /** * Checks if the TTS engine is currently busy by calling the boolean * "working". *

* Returns immediately * * @return the boolean signifying if the engine is busy or not */ public static boolean isWorking() { return working; } /** * Checks if the engine has been instantiated by calling the * boolean "initialized". *

* Returns immediately * * @return the boolean signifying if the engine has been instantiated or not */ public static boolean isInitialized() { return initialized; } /** * If the TTS engine is instantiated, shut it down and set boolean * "initialized" to false. * Log the result. *

* Logs after the TTS engine has been shut down or immediately, * if the TTS engine is not instantiated. */ public static void Shutdown() { if (tts != null) { tts.shutdown(); initialized = false; if (DEBUG) Log.d(TAG, "TTS engine shutdown complete!"); } else { Log.w(TAG, "tts is null!"); } } /** * Starts the private task "speakNative". *

* This method generates multiple logs in Log.d regarding its current state. * * @param speechText the text that is supposed to be read. * @param rate the rate at which the text is supposed to be read. * @param pitch the pitch that gets applied to the Locale/Voice reading the text. * @param inpVolume the volume that gets applied to the Locale/Voice reading the text. * @param voiceName the name of the Locale/Voice reading the text. */ public static void SpeakNative(String speechText, float rate, float pitch, float inpVolume, String voiceName) { if (DEBUG) Log.d(TAG, "SpeakNative called!"); working = true; if (tts != null && initialized) { if (DEBUG) Log.d(TAG, "TTS engine initialized!"); // if (tts.isSpeaking()) { // StopNative(); // } // if (!tts.isSpeaking()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Voice voiceResult = null; if (voiceName != null) { for (Voice voice : tts.getVoices()) { if (voice != null && voiceName.equals((voice.getName()))) { voiceResult = voice; break; } } } if (voiceResult == null) voiceResult = tts.getDefaultVoice(); if (voiceResult == null) { tts.setLanguage(getLocaleFromString(voiceName)); } else { tts.setVoice(voiceResult); } } else { tts.setLanguage(getLocaleFromString(voiceName)); } tts.setSpeechRate(rate); tts.setPitch(pitch); nativeVolume = inpVolume; speakNative(speechText); // } else { // Log.e(TAG, "TTS-system is busy!"); // } } else { Log.e(TAG, "TTS-system not initialized!"); } } /** * Checks if the TTS engine is busy. If it's busy, stop the engine. *

* This method generates a log in Log.d on call and on exit. */ public static void StopNative() { if (DEBUG) Log.d(TAG, "RTVoiceAndroidBridge: StopNative called!"); if (!(tts == null)) { //if (isWorking()) { tts.stop(); if (DEBUG) Log.d(TAG, "RTVoiceAndroidBridge: TTS engine stopped!"); // } else { // Log.w(TAG, "Can't stop the TTS engine as it's not busy!"); // } } else { Log.w(TAG, "Can't stop the TTS engine as there is no instance of it."); } } /** * Generates audio and starts the private task "generateAudio". *

* This method generates multiple logs in Log.d regarding its current state. * * @param speechText the text that is supposed to be read. * @param rate the rate at which the text is supposed to be read. * @param pitch the pitch that gets applied to the Locale/Voice reading the text. * @param voiceName the name of the Locale/Voice that is supposed to read the text. * @param outputFile the target path * @return Multiple Log.d entries, String with the .wav-File path */ public static String Speak(String speechText, float rate, float pitch, String voiceName, String outputFile) { if (DEBUG) Log.d(TAG, "Speak called!"); working = true; String result = null; if (tts != null && initialized) { // if (!tts.isSpeaking()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Voice voiceResult = null; if (voiceName != null) { for (Voice voice : tts.getVoices()) { if (voice != null && voiceName.equals((voice.getName()))) { voiceResult = voice; break; } } } if (voiceResult == null) voiceResult = tts.getDefaultVoice(); if (voiceResult == null) { tts.setLanguage(getLocaleFromString(voiceName)); } else { tts.setVoice(voiceResult); } } else { tts.setLanguage(getLocaleFromString(voiceName)); } tts.setSpeechRate(rate); tts.setPitch(pitch); RTVoiceAndroidBridge.outputFile = outputFile; result = generateAudio(speechText); // } else { // Log.e(TAG, "TTS-system is busy!"); // } } else { Log.e(TAG, "TTS-system not initialized!"); } return result; } /** * Checks if the TTS engine is initialized, then - * - if SDK >= Lollipop: * Looks for installed voices on the Android device and use their names to * generate a for RTVoice readable list. * - if SDK < Lollipop: * Looks for installed locales on the Android device, check each if they * have an available voice to them and use their names and languages to * generate a for RTVoice readable list. *

* It returns a String array when the tasks are done, not immediately. * * @return Multiple Log.d entries, String[] with the available voices/locales */ public static String[] GetVoices() { String[] result = null; if (tts != null && initialized) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Set myVoices = tts.getVoices(); result = new String[myVoices.size()]; int zz = 0; for (Voice voice : myVoices) { if (voice.getName().length() >= 5) { result[zz] = voice.getName() + ";" + voice.getName().substring(0, 5); } else { result[zz] = voice.getName() + ";" + voice.getName(); } zz++; } } else { result = getVoices(); } } else { Log.e(TAG, "TTS-system not initialized!"); } return result; } public static String[] GetEngines() { String[] result = null; if (tts != null && initialized) { List myVoices = tts.getEngines(); result = new String[myVoices.size()]; int zz = 0; for (TextToSpeech.EngineInfo voice : myVoices) { result[zz] = voice.name + ";" + voice.label; zz++; } } else { Log.e(TAG, "TTS-system not initialized!"); } return result; } public static void SetupEngine(String engine) { tts = createTTS(engine); } //endregion //region Private Methods private static TextToSpeech createTTS(String engine) { return new TextToSpeech(RTVoiceAndroidBridge.appContext, new TextToSpeech.OnInitListener() { public void onInit(int status) { //DEBUG if (status == TextToSpeech.SUCCESS) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS successfully executed!"); tts.setOnUtteranceProgressListener(new UtteranceProgressListener() { @Override public void onStart(String s) { if (DEBUG) Log.d(TAG, "TTS: Starting Utterance"); working = true; //reassure it's still true } @Override public void onDone(String s) { if (DEBUG) Log.d(TAG, "TTS: Utterance completed"); working = false; } @Override public void onError(String s) { if (DEBUG) Log.d(TAG, "TTS: A error occurred."); working = false; } }); initialized = true; } else { Log.e(TAG, "Error Code " + status + ""); } if (status == TextToSpeech.ERROR_NETWORK) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS encountered a network problem!"); } if (status == TextToSpeech.ERROR_NETWORK_TIMEOUT) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS encountered a network timeout!"); } if (status == TextToSpeech.ERROR_NOT_INSTALLED_YET) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS doesn't have the requested voice data!"); } if (status == TextToSpeech.ERROR_OUTPUT) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS encountered an error with the output device/file!"); } if (status == TextToSpeech.ERROR_SERVICE) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS encountered a service error!"); } if (status == TextToSpeech.LANG_MISSING_DATA) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS error: Language data is missing!"); } if (status == TextToSpeech.LANG_NOT_SUPPORTED) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS error: Chosen language is not supported!"); } if (status == TextToSpeech.ERROR_INVALID_REQUEST) { if (DEBUG) Log.d(TAG, "Error Code " + status + ": TTS error: Invalid request!"); } } }, engine); } private static void fillLocales() { locales = new HashSet<>(); Locale[] allLocales = Locale.getAvailableLocales(); boolean hasVariant; boolean hasCountry; int res; boolean isLocaleSupported; for (Locale currentLocale : allLocales) { try { res = tts.isLanguageAvailable(currentLocale); hasVariant = (null != currentLocale.getVariant() && currentLocale.getVariant().length() > 0); hasCountry = (null != currentLocale.getCountry() && currentLocale.getCountry().length() > 0); isLocaleSupported = (!hasVariant && !hasCountry && res == TextToSpeech.LANG_AVAILABLE || !hasVariant && hasCountry && res == TextToSpeech.LANG_COUNTRY_AVAILABLE || res == TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE) && currentLocale.toString().length() == 5; if (isLocaleSupported) { locales.add(currentLocale); } } catch (Exception ex) { Log.e(TAG, "Error checking if language is available for TTS (currentLocale=" + currentLocale + "): " + ex.getClass().getSimpleName() + "-" + ex.getMessage()); } } } private static String[] getVoices() { if (locales == null) { fillLocales(); } String[] result = new String[locales.size()]; int zz = 0; for (Locale currentLocale : locales) { result[zz] = currentLocale.getDisplayName() + ";" + currentLocale.toString(); zz++; } return result; } private static String generateAudio(String SpeechText) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { new AsyncTtf().execute(SpeechText); } else { new AsyncTtfDeprecated().execute(SpeechText); } } catch (Exception ex) { Log.e(TAG, "Error generating audio file: " + ex.getClass().getSimpleName() + "-" + ex.getMessage()); } return outputFile; } private static void speakNative(String SpeechText) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { new AsyncTts().execute(SpeechText); } else { new AsyncTtsDeprecated().execute(SpeechText); } } catch (Exception ex) { Log.e(TAG, "Error speaking native: " + ex.getClass().getSimpleName() + "-" + ex.getMessage()); } } private static Locale getLocaleFromString(String localeName) { if (locales == null) { fillLocales(); } Locale result = null; for (Locale locale : locales) { if (locale.getDisplayName().equals((localeName))) { result = locale; break; } } if (result == null) { result = Locale.getDefault(); } return result; } //endregion //region Private Tasks @SuppressWarnings("deprecation") private static class AsyncTtfDeprecated extends AsyncTask { @Override protected Void doInBackground(String... params) { String text = params[0]; HashMap myHashRender = new HashMap<>(); myHashRender.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, text); tts.synthesizeToFile(text, myHashRender, outputFile); working = true; //reassure it's still true return null; } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static class AsyncTtf extends AsyncTask { @Override protected Void doInBackground(String... params) { String text = params[0]; String utteranceId = Integer.toString(this.hashCode()); File destFile = new File(outputFile); tts.synthesizeToFile(text, null, destFile, utteranceId); working = true; //reassure it's still true return null; } } @SuppressWarnings("deprecation") private static class AsyncTtsDeprecated extends AsyncTask { @Override protected Void doInBackground(String... params) { String text = params[0]; HashMap myHashRender = new HashMap<>(); myHashRender.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, text); myHashRender.put(TextToSpeech.Engine.KEY_PARAM_VOLUME, Float.toString(nativeVolume)); tts.speak(text, TextToSpeech.QUEUE_FLUSH, myHashRender); working = true; //reassure it's still true return null; } } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private static class AsyncTts extends AsyncTask { @Override protected Void doInBackground(String... params) { String text = params[0]; String utteranceId = Integer.toString(this.hashCode()); Bundle speakParams = new Bundle(); speakParams.putFloat(TextToSpeech.Engine.KEY_PARAM_VOLUME, nativeVolume); tts.speak(text, TextToSpeech.QUEUE_FLUSH, speakParams, utteranceId); working = true; //reassure it's still true return null; } } //endregion }