#if UNITY_ANDROID || UNITY_EDITOR using UnityEngine; using System.Collections; using System.Linq; namespace Crosstales.RTVoice.Provider { /// Android voice provider. public class VoiceProviderAndroid : BaseVoiceProvider { #region Variables private static string lastEngine; private static bool isInitialized; private static AndroidJavaObject ttsHandler; private static bool isSSML; //>M (API level 23) = true private readonly WaitForSeconds wfs = new WaitForSeconds(0.1f); private System.Collections.Generic.List cachedEngines = new System.Collections.Generic.List(); private bool isLoading; #endregion #region Properties /* /// Returns the singleton instance of this class. /// Singleton instance of this class. public static VoiceProviderAndroid Instance => instance ?? (instance = new VoiceProviderAndroid()); */ public override string AudioFileExtension => ".wav"; public override AudioType AudioFileType => AudioType.WAV; public override string DefaultVoiceName => "English (United States)"; public override bool isWorkingInEditor => false; public override bool isWorkingInPlaymode => false; public override int MaxTextLength => 3999; public override bool isSpeakNativeSupported => true; public override bool isSpeakSupported => true; public override bool isPlatformSupported => Crosstales.RTVoice.Util.Helper.isAndroidPlatform; public override bool isSSMLSupported => isSSML; public override bool isOnlineService => false; public override bool hasCoRoutines => true; public override bool isIL2CPPSupported => true; public override bool hasVoicesInEditor => false; /// Returns all installed TTS engines on Android. public System.Collections.Generic.List Engines => cachedEngines; public override int MaxSimultaneousSpeeches => 0; #endregion #region Constructor public VoiceProviderAndroid() { #if !UNITY_EDITOR || CT_DEVELOP if (!isInitialized) initializeTTS(); #endif } #endregion #region Implemented methods public override void Load(bool forceReload = false) { #if !UNITY_EDITOR || CT_DEVELOP bool _forceReload = forceReload; if (lastEngine != Speaker.Instance.AndroidEngine) { instance = this; isInitialized = false; _forceReload = true; } if (!isInitialized) initializeTTS(); if (cachedVoices?.Count == 0 || _forceReload) { if (!isLoading) { isLoading = true; Speaker.Instance.StartCoroutine(getVoices()); } } else { onVoicesReady(); } #endif } public override IEnumerator SpeakNative(Crosstales.RTVoice.Model.Wrapper wrapper) { #if !UNITY_EDITOR || CT_DEVELOP if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty!"); } else { yield return null; //return to the main process (uid) if (!isInitialized) { do { // waiting... yield return wfs; } while (!(isInitialized = ttsHandler.CallStatic("isInitialized"))); } string voiceName = getVoiceName(wrapper); silence = false; onSpeakStart(wrapper); ttsHandler.CallStatic("SpeakNative", prepareText(wrapper), wrapper.Rate, wrapper.Pitch, wrapper.Volume, voiceName); do { yield return wfs; } while (!silence && ttsHandler.CallStatic("isWorking")); if (Crosstales.RTVoice.Util.Config.DEBUG) Debug.Log("Text spoken: " + wrapper.Text); onSpeakComplete(wrapper); } } #else yield return null; #endif } public override IEnumerator Speak(Crosstales.RTVoice.Model.Wrapper wrapper) { #if !UNITY_EDITOR || CT_DEVELOP if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { if (wrapper.Source == null) { Debug.LogWarning("'wrapper.Source' is null: " + wrapper); } else { yield return null; //return to the main process (uid) if (!isInitialized) { do { // waiting... yield return wfs; } while (!(isInitialized = ttsHandler.CallStatic("isInitialized"))); } string voiceName = getVoiceName(wrapper); string outputFile = getOutputFile(wrapper.Uid, true); ttsHandler.CallStatic("Speak", prepareText(wrapper), wrapper.Rate, wrapper.Pitch, voiceName, outputFile); silence = false; onSpeakAudioGenerationStart(wrapper); do { yield return wfs; } while (!silence && ttsHandler.CallStatic("isWorking")); yield return playAudioFile(wrapper, Crosstales.Common.Util.NetworkHelper.ValidURLFromFilePath(outputFile), outputFile); } } } #else yield return null; #endif } public override IEnumerator Generate(Crosstales.RTVoice.Model.Wrapper wrapper) { #if !UNITY_EDITOR || CT_DEVELOP if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { yield return null; //return to the main process (uid) if (!isInitialized) { do { // waiting... yield return wfs; } while (!(isInitialized = ttsHandler.CallStatic("isInitialized"))); } string voiceName = getVoiceName(wrapper); string outputFile = getOutputFile(wrapper.Uid, true); ttsHandler.CallStatic("Speak", prepareText(wrapper), wrapper.Rate, wrapper.Pitch, voiceName, outputFile); silence = false; onSpeakAudioGenerationStart(wrapper); do { yield return wfs; } while (!silence && ttsHandler.CallStatic("isWorking")); processAudioFile(wrapper, outputFile); } } #else yield return null; #endif } #if !UNITY_EDITOR || CT_DEVELOP public override void Silence() { ttsHandler.CallStatic("StopNative"); base.Silence(); } #endif #endregion #region Public methods public static void ShutdownTTS() { #if !UNITY_EDITOR || CT_DEVELOP ttsHandler.CallStatic("Shutdown"); #endif } #endregion #region Private methods #if !UNITY_EDITOR || CT_DEVELOP private IEnumerator getVoices() { yield return null; if (!isInitialized) { do { yield return wfs; } while (!(isInitialized = ttsHandler.CallStatic("isInitialized"))); } string[] stringVoices = null; bool success = false; try { stringVoices = ttsHandler.CallStatic("GetVoices"); success = true; } catch (System.Exception ex) { string errorMessage = "Could not get any voices!" + System.Environment.NewLine + ex; Debug.LogError(errorMessage); onErrorInfo(null, errorMessage); } if (success) { System.Collections.Generic.List voices = new System.Collections.Generic.List(350); foreach (string voice in stringVoices) { string[] currentVoiceData = voice.Split(';'); if (!currentVoiceData[0].CTContains("network")) //ignore network-voices { Crosstales.RTVoice.Model.Enum.Gender gender = Crosstales.RTVoice.Model.Enum.Gender.UNKNOWN; if (currentVoiceData[0].CTContains("#male")) { gender = Crosstales.RTVoice.Model.Enum.Gender.MALE; } else if (currentVoiceData[0].CTContains("#female")) { gender = Crosstales.RTVoice.Model.Enum.Gender.FEMALE; } if (gender == Crosstales.RTVoice.Model.Enum.Gender.UNKNOWN) { } string name = currentVoiceData[0]; voices.Add(new Crosstales.RTVoice.Model.Voice(name, "Android voice: " + voice, Crosstales.RTVoice.Util.Helper.AndroidVoiceNameToGender(name), "unknown", currentVoiceData[1])); } } cachedVoices = voices.OrderBy(s => s.Name).ToList(); if (Crosstales.RTVoice.Util.Constants.DEV_DEBUG) Debug.Log("Voices read: " + cachedVoices.CTDump()); } yield return getEngines(); isLoading = false; onVoicesReady(); } private IEnumerator getEngines() { string[] stringEngines = null; bool success = false; try { stringEngines = ttsHandler.CallStatic("GetEngines"); success = true; } catch (System.Exception ex) { string errorMessage = "Could not get any engines!" + System.Environment.NewLine + ex; Debug.LogWarning(errorMessage); onErrorInfo(null, errorMessage); } if (success) { yield return null; System.Collections.Generic.List engines = stringEngines.Select(voice => voice.Split(';')).Select(currentEngineData => currentEngineData[0]).ToList(); cachedEngines = engines.OrderBy(s => s).ToList(); if (Crosstales.RTVoice.Util.Constants.DEV_DEBUG) Debug.Log("Engines read: " + cachedEngines.CTDump()); } } private static void initializeTTS() { AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); AndroidJavaObject jo = jc.GetStatic("currentActivity"); ttsHandler = new AndroidJavaObject("com.crosstales.RTVoice.RTVoiceAndroidBridge", jo); ttsHandler.CallStatic("SetupEngine", Speaker.Instance.AndroidEngine); lastEngine = Speaker.Instance.AndroidEngine; isSSML = ttsHandler.CallStatic("isSSMLSupported"); } private static string prepareText(Crosstales.RTVoice.Model.Wrapper wrapper) { //TEST //wrapper.ForceSSML = false; if (isSSML && wrapper.ForceSSML && !Speaker.Instance.AutoClearTags) { System.Text.StringBuilder sbXML = new System.Text.StringBuilder(); //sbXML.Append(""); //sbXML.Append(""); sbXML.Append(""); /* //Volume seems to have no effect! if (wrapper.Volume < 1f) { sbXML.Append(""); } */ sbXML.Append(wrapper.Text); /* if (wrapper.Volume < 1f) sbXML.Append(""); */ sbXML.Append(""); return getValidXML(sbXML.ToString().Replace('"', '\'')); } return wrapper.Text.Replace('"', '\''); } #endif #endregion #region Editor-only methods #if UNITY_EDITOR public override void GenerateInEditor(Crosstales.RTVoice.Model.Wrapper wrapper) { Debug.LogError("'GenerateInEditor' is not supported for Android!"); } public override void SpeakNativeInEditor(Crosstales.RTVoice.Model.Wrapper wrapper) { Debug.LogError("'SpeakNativeInEditor' is not supported for Android!"); } #endif #endregion } } #endif // © 2016-2023 crosstales LLC (https://www.crosstales.com)