592 lines
18 KiB
Java
592 lines
18 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.Locale;
|
|
import java.util.Set;
|
|
import java.util.List;
|
|
import java.util.ArrayList;
|
|
|
|
//endregion
|
|
|
|
/**
|
|
* RTVoiceAndroidBridge.java
|
|
* Version 2022.2.1
|
|
*
|
|
* Acts as a handler for all TTS functions called by RT-Voice on Android.
|
|
*
|
|
* © 2016-2022 crosstales LLC (https://www.crosstales.com)
|
|
*/
|
|
public class RTVoiceAndroidBridge {
|
|
|
|
//region Variables
|
|
|
|
//Context to instantiate TTS engine
|
|
private static Context appContext;
|
|
|
|
//TTS object
|
|
private static TextToSpeech tts;
|
|
|
|
private static Set<Voice> voices;
|
|
|
|
//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
|
|
|
|
public static boolean isSSMLSupported() {
|
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
try {
|
|
tts.shutdown();
|
|
initialized = false;
|
|
|
|
if (DEBUG)
|
|
Log.d(TAG, "TTS engine shutdown complete!");
|
|
} catch (Exception ex) {
|
|
Log.e(TAG, "Shutdown of TTS engine failed: " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
|
|
}
|
|
} 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
Voice voiceResult = null;
|
|
|
|
if (voiceName != null) {
|
|
for (Voice voice : voices) {
|
|
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 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)) {
|
|
tts.stop();
|
|
|
|
if (DEBUG)
|
|
Log.d(TAG, "RTVoiceAndroidBridge: TTS engine stopped!");
|
|
} 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 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
Voice voiceResult = null;
|
|
|
|
if (voiceName != null) {
|
|
for (Voice voice : voices) {
|
|
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 not initialized!");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Checks if the TTS engine is initialized:
|
|
* - if SDK >= M:
|
|
* Looks for installed voices on the Android device and use their names to generate a for RTVoice readable list.
|
|
* - if SDK < M:
|
|
* 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 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.M) {
|
|
List<String> list = new ArrayList<String>();
|
|
|
|
for (Voice voice : voices) {
|
|
if (voice != null) {
|
|
//if (voice.getName().length() >= 5) {
|
|
// list.add(voice.getName() + ";" + voice.getName().substring(0, 5));
|
|
//} else {
|
|
list.add(voice.getName() + ";" + voice.getLocale().toString());
|
|
//}
|
|
}
|
|
}
|
|
|
|
result = list.toArray(new String[0]);
|
|
} else {
|
|
result = getVoices();
|
|
}
|
|
} else {
|
|
Log.e(TAG, "TTS-system not initialized!");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Returns the available TTS engines.
|
|
*
|
|
* @return String[] with the available TTS engines, like the default "com.google.android.tts"
|
|
*/
|
|
public static String[] GetEngines() {
|
|
String[] result = null;
|
|
|
|
if (tts != null && initialized) {
|
|
try {
|
|
List<TextToSpeech.EngineInfo> engines = tts.getEngines();
|
|
|
|
result = new String[engines.size()];
|
|
int zz = 0;
|
|
|
|
for (TextToSpeech.EngineInfo engine : engines) {
|
|
result[zz] = engine.name + ";" + engine.label;
|
|
zz++;
|
|
}
|
|
} catch (Exception ex) {
|
|
Log.e(TAG, "Error getting engines: " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
|
|
}
|
|
} else {
|
|
Log.e(TAG, "TTS-system not initialized!");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a specific TTS engine.
|
|
*
|
|
* @param engine TTS engine to be used
|
|
* @return String[] with the available TTS engines
|
|
*/
|
|
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) {
|
|
if (status == TextToSpeech.SUCCESS) {
|
|
try {
|
|
if (DEBUG)
|
|
Log.d(TAG, "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;
|
|
}
|
|
});
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
voices = tts.getVoices();
|
|
}
|
|
initialized = true;
|
|
} catch (Exception ex) {
|
|
Log.e(TAG, "Could not start utterance: " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
|
|
}
|
|
} 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.M) {
|
|
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.M) {
|
|
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.M)
|
|
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.M)
|
|
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
|
|
|
|
}
|