572 lines
19 KiB
Java
572 lines
19 KiB
Java
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.
|
|
* <p>
|
|
* 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<Locale> 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".
|
|
* <p>
|
|
* 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".
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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".
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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".
|
|
* <p>
|
|
* 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.
|
|
* <p>
|
|
* 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<Voice> 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<TextToSpeech.EngineInfo> 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<String, Void, Void> {
|
|
@Override
|
|
protected Void doInBackground(String... params) {
|
|
String text = params[0];
|
|
HashMap<String, String> 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<String, Void, Void> {
|
|
@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<String, Void, Void> {
|
|
@Override
|
|
protected Void doInBackground(String... params) {
|
|
String text = params[0];
|
|
HashMap<String, String> 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<String, Void, Void> {
|
|
@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
|
|
|
|
}
|