using UnityEngine; using System.Linq; using UnityEngine.Serialization; namespace Crosstales.RTVoice { /// Main component of RT-Voice. [ExecuteInEditMode] [DisallowMultipleComponent] [RequireComponent(typeof(LiveSpeaker))] [HelpURL("https://www.crosstales.com/media/data/assets/rtvoice/api/class_crosstales_1_1_r_t_voice_1_1_speaker.html")] public class Speaker : Crosstales.Common.Util.Singleton { #region Variables [FormerlySerializedAs("CustomProvider")] [Header("Custom Provider"), Tooltip("Custom provider for RT-Voice."), SerializeField] private Provider.BaseCustomVoiceProvider customProvider; [FormerlySerializedAs("CustomMode")] [Tooltip("Enable or disable the custom provider (default: false)."), SerializeField] private bool customMode; [FormerlySerializedAs("ESpeakMode")] [Header("eSpeak Settings"), Tooltip("Enable or disable eSpeak for standalone platforms (default: false)."), SerializeField] private bool eSpeakMode; [Tooltip("eSpeak application name/path (default: 'espeak')."), SerializeField] private string eSpeakApplication = "espeak"; [Tooltip("eSpeak application data path (default: empty)."), SerializeField] private string eSpeakDataPath = string.Empty; [FormerlySerializedAs("ESpeakModifier")] [Tooltip("Active modifier for all eSpeak voices (default: none, m1-m6 = male, f1-f4 = female)."), SerializeField] private Model.Enum.ESpeakModifiers eSpeakModifier = Model.Enum.ESpeakModifiers.none; [FormerlySerializedAs("AndroidEngine")] [Header("Android Settings"), Tooltip("Active speech engine under Android (default: empty)."), SerializeField] private string androidEngine = string.Empty; [FormerlySerializedAs("AutoClearTags")] [Header("Advanced Settings"), Tooltip("Automatically clear tags from speeches depending on the capabilities of the current TTS-system (default: false)."), SerializeField] private bool autoClearTags; [FormerlySerializedAs("Caching"), Tooltip("Enable or disable the caching of generated speeches (default: true)."), SerializeField] private bool caching = true; [FormerlySerializedAs("SilenceOnDisable")] [Header("Behaviour Settings"), Tooltip("Silence any speeches if this component gets disabled (default: false)."), SerializeField] private bool silenceOnDisable; [FormerlySerializedAs("SilenceOnFocusLost")] [FormerlySerializedAs("SilenceOnFocustLost")] [Tooltip("Silence any speeches if the application loses the focus. Otherwise the speeches are paused and unpaused (default: false)."), SerializeField] private bool silenceOnFocusLost; [Tooltip("Starts and stops the Speaker depending on the focus and running state (default: true)."), SerializeField] private bool handleFocus = true; /* /// Files to delete at the application end. public static readonly System.Collections.Generic.List FilesToDelete = new System.Collections.Generic.List(); */ private readonly System.Collections.Generic.Dictionary removeSources = new System.Collections.Generic.Dictionary(); private float cleanUpTimer; private Provider.IVoiceProvider voiceProvider; private Provider.MainVoiceProvider mainVoiceProvider; private Provider.BaseCustomVoiceProvider customVoiceProvider; private readonly System.Collections.Generic.Dictionary genericSources = new System.Collections.Generic.Dictionary(); private readonly System.Collections.Generic.Dictionary providedSources = new System.Collections.Generic.Dictionary(); private int speechCount; private int busyCount; private bool deleted; //ignore in reset! private static readonly char[] splitCharWords = {' '}; private const float cleanUpTime = 5f; //in seconds private System.Threading.Thread deleteWorker; private static bool loggedVPIsNull; #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR private static string currentTextToSpeak; private static Model.Wrapper currentWrapper; private const float delayPause = 0.5f; private static float lastTimePaused = 0; #endif #endregion #region Properties /// Custom provider for RT-Voice. public Provider.BaseCustomVoiceProvider CustomProvider { get => customProvider; set { if (customProvider == value) return; customProvider = value; ReloadProvider(); } } /// Enables or disables the custom provider. public bool CustomMode { get => customMode; set { if (customMode == value) return; customMode = value; ReloadProvider(); } } /// Enable or disable eSpeak for standalone platforms. public bool ESpeakMode { get => eSpeakMode; set { if (eSpeakMode == value) return; eSpeakMode = value; ReloadProvider(); } } /// eSpeak application name/path. public string ESpeakApplication { get => eSpeakApplication; set => eSpeakApplication = value; } /// eSpeak application data path. public string ESpeakDataPath { get => eSpeakDataPath; set => eSpeakDataPath = value; } /// Active modifier for all eSpeak voices. public Model.Enum.ESpeakModifiers ESpeakModifier { get => eSpeakModifier; set => eSpeakModifier = value; } /// Active speech engine under Android. public string AndroidEngine { get => androidEngine; set { if (androidEngine == value) return; androidEngine = value; ReloadProvider(); } } /// Automatically clear tags from speeches depending on the capabilities of the current TTS-system. public bool AutoClearTags { get => autoClearTags; set => autoClearTags = value; } /// Enable or disable the caching of generated speeches. public bool Caching { get => caching; set => caching = value; } /// Silence any speeches if this component gets disabled. public bool SilenceOnDisable { get => silenceOnDisable; set => silenceOnDisable = value; } /// Silence any speeches if the application loses the focus. public bool SilenceOnFocusLost { get => silenceOnFocusLost; set => silenceOnFocusLost = value; } /// Starts and stops the Speaker depending on the focus and running state. public bool HandleFocus { get => handleFocus; set => handleFocus = value; } /* /// Don't destroy gameobject during scene switches. public bool DontDestroy { get => dontDestroy; set => dontDestroy = value; } */ /// Number of active speeches. public int SpeechCount { get => speechCount; private set => speechCount = value < 0 ? 0 : value; } /// Number of active calls. public int BusyCount { get => busyCount; private set => busyCount = value < 0 ? 0 : value; } /// Are all voices ready to speak? public bool areVoicesReady { get; private set; } /// Checks if TTS is available on this system. /// True if TTS is available on this system. public bool isTTSAvailable { get { if (voiceProvider != null) return voiceProvider.Voices.Count > 0; logVPIsNull(); return false; } } /// Checks if RT-Voice is speaking on this system. /// True if RT-Voice is speaking on this system. public bool isSpeaking => SpeechCount > 0; /// Checks if RT-Voice is busy on this system. /// True if RT-Voice is busy on this system. public bool isBusy => BusyCount > 0; /// Is standalone TTS enforced? public bool enforcedStandaloneTTS { get; private set; } /// Is RT-Voice paused? public bool isPaused { get; private set; } /// Is RT-Voice muted? public bool isMuted { get; private set; } #region Provider delegates /// Returns the extension of the generated audio files. /// Extension of the generated audio files. public string AudioFileExtension { get { if (voiceProvider != null) return voiceProvider.AudioFileExtension; logVPIsNull(); return ".wav"; //best guess } } /// Returns the default voice name of the current TTS-provider. /// Default voice name of the current TTS-provider. public string DefaultVoiceName { get { if (voiceProvider != null) return voiceProvider.DefaultVoiceName; logVPIsNull(); return string.Empty; } } /// Get all available voices from the current TTS-system. /// All available voices (alphabetically ordered by 'Name') as a list. public System.Collections.Generic.List Voices { get { //Debug.Log($"Voices: {voiceProvider}"); if (voiceProvider != null) return voiceProvider.Voices; logVPIsNull(); return new System.Collections.Generic.List(); } } /// Indicates if this TTS-system is working directly inside the Unity Editor (without 'Play'-mode). /// True if this TTS-system is working directly inside the Unity Editor. public bool isWorkingInEditor { get { if (voiceProvider != null) return voiceProvider.isWorkingInEditor; logVPIsNull(); return false; } } /// Indicates if this TTS-system is working with 'Play'-mode inside the Unity Editor. /// True if this TTS-system is working with 'Play'-mode inside the Unity Editor. public bool isWorkingInPlaymode { get { if (voiceProvider != null) return voiceProvider.isWorkingInPlaymode; logVPIsNull(); return false; } } /// Maximal length of the speech text (in characters) for the current TTS-system. /// The maximal length of the speech text. public int MaxTextLength { get { if (voiceProvider != null) return voiceProvider.MaxTextLength; logVPIsNull(); return 3999; //minimum (Android) } } /// Indicates if this TTS-system is supporting SpeakNative. /// True if this TTS-system supports SpeakNative. public bool isSpeakNativeSupported { get { if (voiceProvider != null) return voiceProvider.isSpeakNativeSupported; logVPIsNull(); return false; } } /// Indicates if this TTS-system is supporting Speak. /// True if this TTS-system supports Speak. public bool isSpeakSupported { get { if (voiceProvider != null) return voiceProvider.isSpeakSupported; logVPIsNull(); return false; } } /// Indicates if this TTS-system is supporting the current platform. /// True if this TTS-system supports current platform. public bool isPlatformSupported => voiceProvider?.isPlatformSupported == true; /// Indicates if this TTS-system is supporting SSML. /// True if this TTS-system supports SSML. public bool isSSMLSupported { get { if (voiceProvider != null) return voiceProvider.isSSMLSupported; logVPIsNull(); return false; } } /// Indicates if this TTS-system is an online service like MaryTTS or AWS Polly. /// True if this TTS-system is an online service. public bool isOnlineService { get { if (voiceProvider != null) return voiceProvider.isOnlineService; logVPIsNull(); return false; } } /// Indicates if this TTS-system uses co-routines. /// True if this TTS-system uses co-routines. public bool hasCoRoutines { get { if (voiceProvider != null) return voiceProvider.hasCoRoutines; logVPIsNull(); return true; } } /// Indicates if this TTS-system is supporting IL2CPP. /// True if this TTS-system supports IL2CPP. public bool isIL2CPPSupported { get { if (voiceProvider != null) return voiceProvider.isIL2CPPSupported; logVPIsNull(); return true; } } /// Indicates if this provider returns voices in the Editor mode. /// True if this provider returns voices in the Editor mode. public bool hasVoicesInEditor { get { if (voiceProvider != null) return voiceProvider.hasVoicesInEditor; logVPIsNull(); return false; } } /// Get all available cultures from the current TTS-system (ISO 639-1). /// All available cultures (alphabetically ordered by 'Culture') as a list. public System.Collections.Generic.List Cultures { get { if (voiceProvider != null) return voiceProvider.Cultures; logVPIsNull(); return new System.Collections.Generic.List(); } } /// Get all available speech engines (works only for Android). /// All available speech engines as a list. public System.Collections.Generic.List Engines { get { #if UNITY_ANDROID || UNITY_EDITOR if (voiceProvider is Crosstales.RTVoice.Provider.VoiceProviderAndroid android) return android.Engines; logVPIsNull(); #endif return new System.Collections.Generic.List(); } } #endregion #endregion #region Events [Header("Events")] public VoicesReadyEvent OnReady; public SpeakStartEvent OnSpeakStarted; public SpeakCompleteEvent OnSpeakCompleted; public ProviderChangeEvent OnProviderChanged; public ErrorEvent OnError; /// An event triggered whenever the voices of a provider are ready. public event VoicesReady OnVoicesReady; /// An event triggered whenever a speak is started. public event SpeakStart OnSpeakStart; /// An event triggered whenever a speak is completed. public event SpeakComplete OnSpeakComplete; /// An event triggered whenever a new word is spoken (native, Windows and iOS only). public event SpeakCurrentWord OnSpeakCurrentWord; /// An event triggered whenever a new phoneme is spoken (native, Windows only). public event SpeakCurrentPhoneme OnSpeakCurrentPhoneme; /// An event triggered whenever a new viseme is spoken (native, Windows only). public event SpeakCurrentViseme OnSpeakCurrentViseme; /// An event triggered whenever a speak audio generation is started. public event SpeakAudioGenerationStart OnSpeakAudioGenerationStart; /// An event triggered whenever a speak audio generation is completed. public event SpeakAudioGenerationComplete OnSpeakAudioGenerationComplete; /// An event triggered whenever a provider changes (e.g. Windows to MaryTTS). public event ProviderChange OnProviderChange; /// An event triggered whenever an error occurs. public event ErrorInfo OnErrorInfo; #endregion #region MonoBehaviour methods protected override void Awake() { base.Awake(); if (instance == this) { if (!deleted) { deleted = true; //if (Util.Helper.isWindowsPlatform && Util.Config.AUDIOFILE_AUTOMATIC_DELETE) //only delete files under Windows if (Util.Config.AUDIOFILE_AUTOMATIC_DELETE) DeleteAudioFiles(); } if (Util.Helper.isLinuxPlatform) eSpeakMode = true; /* if (isESpeakMode && !Util.Helper.isStandalonePlatform) ESpeakMode = false; */ initProvider(); } } private void Update() { cleanUpTimer += Time.deltaTime; if (cleanUpTimer > cleanUpTime) { cleanUpTimer = 0f; if (genericSources.Count > 0) { foreach (System.Collections.Generic.KeyValuePair source in genericSources.Where(source => source.Value != null && source.Value.clip != null && !Common.Util.BaseHelper.hasActiveClip(source.Value))) { removeSources.Add(source.Key, source.Value); } foreach (System.Collections.Generic.KeyValuePair source in removeSources) { genericSources.Remove(source.Key); Destroy(source.Value); } removeSources.Clear(); } if (providedSources.Count > 0) { foreach (System.Collections.Generic.KeyValuePair source in providedSources.Where(source => source.Value != null && source.Value.clip != null && !Common.Util.BaseHelper.hasActiveClip(source.Value))) { source.Value.clip = null; //remove clip removeSources.Add(source.Key, source.Value); } foreach (System.Collections.Generic.KeyValuePair source in removeSources) { //genericSources.Remove(source.Key); providedSources.Remove(source.Key); } removeSources.Clear(); } } } private void OnDisable() { if (silenceOnDisable) Silence(); } protected override void OnDestroy() { Silence(); if (instance == this) { unsubscribeEvents(); unsubscribeCustomEvents(); } base.OnDestroy(); } protected override void OnApplicationQuit() { Silence(); #if UNITY_ANDROID || UNITY_EDITOR if (voiceProvider is Crosstales.RTVoice.Provider.VoiceProviderAndroid) Provider.VoiceProviderAndroid.ShutdownTTS(); #endif /* if (!Util.Helper.isEditorMode) { foreach (string outputFile in FilesToDelete) { if (System.IO.File.Exists(outputFile)) { try { System.IO.File.Delete(outputFile); } catch (System.Exception ex) { string errorMessage = "Could not delete file '" + outputFile + "'!" + System.Environment.NewLine + ex; Debug.LogError(errorMessage, this); } } } } */ #if !UNITY_WSA || UNITY_EDITOR if (deleteWorker?.IsAlive == true) { if (Util.Constants.DEV_DEBUG) Debug.Log("Killing worker", this); deleteWorker.Abort(); //TODO dangerous - find a better solution! } #endif base.OnApplicationQuit(); } private void OnApplicationFocus(bool hasFocus) { if (Util.Helper.isMobilePlatform || !Application.runInBackground) { #if UNITY_ANDROID || UNITY_IOS if (!TouchScreenKeyboard.isSupported || !TouchScreenKeyboard.visible) { #endif if (silenceOnFocusLost) { if (!hasFocus) Silence(); } else { if (handleFocus) { if (hasFocus) { UnPause(); } else { Pause(); } } } #if UNITY_ANDROID || UNITY_IOS } #endif } } #endregion #region Static methods /// Resets this object. //[RuntimeInitializeOnLoadMethod] public static void ResetObject() { DeleteInstance(); loggedVPIsNull = false; } /// /// Approximates the speech length in seconds of a given text and rate. /// Note: This is an experimental method and doesn't provide an exact value; +/- 15% is "normal"! /// /// Text for the length approximation. /// Speech rate of the speaker in percent for the length approximation (1 = 100%, default: 1, optional). /// Words per minute (default: 175, optional). /// Time factor for the calculated value (default: 0.9, optional). /// Approximated speech length in seconds of the given text and rate. public float ApproximateSpeechLength(string text, float rate = 1f, float wordsPerMinute = 175f, float timeFactor = 0.9f) { float words = text.Split(splitCharWords, System.StringSplitOptions.RemoveEmptyEntries).Length; float characters = text.Length - words + 1; float ratio = characters / words; if (Common.Util.BaseHelper.isWindowsPlatform && !ESpeakMode && !CustomMode) { if (Mathf.Abs(rate - 1f) > Common.Util.BaseConstants.FLOAT_TOLERANCE) { //relevant? if (rate > 1f) { //larger than 1 if (rate >= 2.75f) { rate = 2.78f; } else if (rate >= 2.6f && rate < 2.75f) { rate = 2.6f; } else if (rate >= 2.35f && rate < 2.6f) { rate = 2.39f; } else if (rate >= 2.2f && rate < 2.35f) { rate = 2.2f; } else if (rate >= 2f && rate < 2.2f) { rate = 2f; } else if (rate >= 1.8f && rate < 2f) { rate = 1.8f; } else if (rate >= 1.6f && rate < 1.8f) { rate = 1.6f; } else if (rate >= 1.4f && rate < 1.6f) { rate = 1.45f; } else if (rate >= 1.2f && rate < 1.4f) { rate = 1.28f; } else if (rate > 1f && rate < 1.2f) { rate = 1.14f; } } else { //smaller than 1 if (rate <= 0.3f) { rate = 0.33f; } else if (rate > 0.3 && rate <= 0.4f) { rate = 0.375f; } else if (rate > 0.4 && rate <= 0.45f) { rate = 0.42f; } else if (rate > 0.45 && rate <= 0.5f) { rate = 0.47f; } else if (rate > 0.5 && rate <= 0.55f) { rate = 0.525f; } else if (rate > 0.55 && rate <= 0.6f) { rate = 0.585f; } else if (rate > 0.6 && rate <= 0.7f) { rate = 0.655f; } else if (rate > 0.7 && rate <= 0.8f) { rate = 0.732f; } else if (rate > 0.8 && rate <= 0.9f) { rate = 0.82f; } else if (rate > 0.9 && rate < 1f) { rate = 0.92f; } } } } float speechLength = words / (wordsPerMinute / 60 * rate); if (ratio < 2) { speechLength *= 1f; } else if (ratio >= 2f && ratio < 3f) { speechLength *= 1.05f; } else if (ratio >= 3f && ratio < 3.5f) { speechLength *= 1.15f; } else if (ratio >= 3.5f && ratio < 4f) { speechLength *= 1.2f; } else if (ratio >= 4f && ratio < 4.5f) { speechLength *= 1.25f; } else if (ratio >= 4.5f && ratio < 5f) { speechLength *= 1.3f; } else if (ratio >= 5f && ratio < 5.5f) { speechLength *= 1.4f; } else if (ratio >= 5.5f && ratio < 6f) { speechLength *= 1.45f; } else if (ratio >= 6f && ratio < 6.5f) { speechLength *= 1.5f; } else if (ratio >= 6.5f && ratio < 7f) { speechLength *= 1.6f; } else if (ratio >= 7f && ratio < 8f) { speechLength *= 1.7f; } else if (ratio >= 8f && ratio < 9f) { speechLength *= 1.8f; } else { speechLength *= ratio * (ratio / 100f + 0.02f) + 1f; } if (speechLength < 0.8f) speechLength += 0.6f; return speechLength * timeFactor; } /// Is a voice available for a given gender and optional culture from the current TTS-system? /// Gender of the voice /// Culture of the voice (e.g. "en", optional) /// True if a voice is available for a given gender and culture. public bool isVoiceForGenderAvailable(Model.Enum.Gender gender, string culture = "") { return VoicesForGender(gender, culture).Count > 0; } /// Get all available voices for a given gender and optional culture from the current TTS-system. /// Gender of the voice /// Culture of the voice (e.g. "en", optional) /// Always returns voices if there is no match with the gender and/or culture (default: false, optional) /// All available voices (alphabetically ordered by 'Name') for a given gender and culture as a list. public System.Collections.Generic.List VoicesForGender(Model.Enum.Gender gender, string culture = "", bool isFuzzy = false) { System.Collections.Generic.List voices = new System.Collections.Generic.List(Voices.Count); if (string.IsNullOrEmpty(culture)) { if (Model.Enum.Gender.UNKNOWN == gender) { return Voices; } voices.AddRange(Voices.Where(voice => voice.Gender == gender)); } else { if (Model.Enum.Gender.UNKNOWN == gender) { return VoicesForCulture(culture, isFuzzy); } voices.AddRange(VoicesForCulture(culture, isFuzzy).Where(voice => voice.Gender == gender)); } return voices; } /// Get a voice from for a given gender and optional culture and optional index from the current TTS-system. /// Gender of the voice /// Culture of the voice (e.g. "en", optional) /// Index of the voice (default: 0, optional) /// Fallback culture of the voice (e.g. "en", default "", optional) /// Always returns voices if there is no match with the gender and/or culture (default: false, optional) /// Voice for the given culture and index. public Model.Voice VoiceForGender(Model.Enum.Gender gender, string culture = "", int index = 0, string fallbackCulture = "", bool isFuzzy = false) { Model.Voice result = null; System.Collections.Generic.List voices = VoicesForGender(gender, culture, isFuzzy); if (voices.Count > 0) { if (voices.Count - 1 >= index && index >= 0) { result = voices[index]; } else { //use the default voice //result = voices[0]; Debug.LogWarning($"No voice for gender '{gender}' and culture '{culture}' with index {index} found! Speaking with the default voice!", this); } } else { voices = VoicesForGender(gender, fallbackCulture, isFuzzy); if (voices.Count > 0) { result = voices[0]; Debug.LogWarning($"No voice for gender '{gender}' and culture '{culture}' found! Speaking with the fallback culture: '{fallbackCulture}'", this); } else { //use the default voice Debug.LogWarning($"No voice for gender '{gender}' and culture '{culture}' found! Speaking with the default voice!", this); } } return result; } /// Is a voice available for a given culture from the current TTS-system? /// Culture of the voice (e.g. "en") /// True if a voice is available for a given culture. public bool isVoiceForCultureAvailable(string culture) { return VoicesForCulture(culture).Count > 0; } /// Get all available voices for a given culture from the current TTS-system. /// Culture of the voice (e.g. "en") /// Always returns voices if there is no match with the culture (default: false, optional) /// All available voices (alphabetically ordered by 'Name') for a given culture as a list. public System.Collections.Generic.List VoicesForCulture(string culture, bool isFuzzy = false) { if (string.IsNullOrEmpty(culture)) { if (Util.Config.DEBUG) Debug.LogWarning("The given 'culture' is null or empty! Returning all available voices.", this); return Voices; } string _culture = culture.Trim().Replace(" ", string.Empty).Replace("_", string.Empty).Replace("-", string.Empty); #if UNITY_WSA System.Collections.Generic.List voices = Voices.Where(s => s.SimplifiedCulture.StartsWith(_culture, System.StringComparison.OrdinalIgnoreCase)).OrderBy(s => s.Name).ToList(); #else System.Collections.Generic.List voices = Voices.Where(s => s.SimplifiedCulture.StartsWith(_culture, System.StringComparison.InvariantCultureIgnoreCase)).OrderBy(s => s.Name).ToList(); #endif if (voices.Count == 0 && isFuzzy) { return Voices; } return voices; } /// Get a voice from for a given culture and optional index from the current TTS-system. /// Culture of the voice (e.g. "en") /// Index of the voice (default: 0, optional) /// Fallback culture of the voice (e.g. "en", default "", optional) /// Always returns voices if there is no match with the culture (default: false, optional) /// Voice for the given culture and index. public Model.Voice VoiceForCulture(string culture, int index = 0, string fallbackCulture = "", bool isFuzzy = false) { Model.Voice result = null; if (!string.IsNullOrEmpty(culture)) { System.Collections.Generic.List voices = VoicesForCulture(culture, isFuzzy); if (voices.Count > 0) { if (voices.Count - 1 >= index && index >= 0) { result = voices[index]; } else { //use the default voice //result = voices[0]; Debug.LogWarning($"No voices for culture '{culture}' with index {index} found! Speaking with the default voice!", this); } } else { voices = VoicesForCulture(fallbackCulture, isFuzzy); if (voices.Count > 0) { result = voices[0]; Debug.LogWarning($"No voices for culture '{culture}' found! Speaking with the fallback culture: '{fallbackCulture}'", this); } else { //use the default voice Debug.LogWarning($"No voices for culture '{culture}' found! Speaking with the default voice!", this); } } } return result; } /// Is a voice available for a given name from the current TTS-system? /// Name of the voice (e.g. "Alex") /// Exact match for the voice name (default: false, optional) /// True if a voice is available for a given name. public bool isVoiceForNameAvailable(string _name, bool isExact = false) { return VoiceForName(_name, isExact) != null; } /// Get a voice for a given name from the current TTS-system. /// Name of the voice (e.g. "Alex") /// Exact match for the voice name (default: false, optional) /// Voice for the given name or null if not found. public Model.Voice VoiceForName(string _name, bool isExact = false) { Model.Voice result = null; if (string.IsNullOrEmpty(_name)) { Debug.LogWarning("The given 'name' is null or empty! Returning null.", this); } else { result = isExact ? Voices.FirstOrDefault(voice => voice.Name.CTEquals(_name)) : Voices.FirstOrDefault(voice => voice.Name.CTContains(_name)); if (result == null) { //use the default voice Debug.LogWarning("No voice for name '" + _name + "' found! Speaking with the default voice!", this); } } return result; } /// Speaks a text with a given voice (native mode). /// Text to speak. /// Voice to speak (optional). /// Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional). /// Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional). /// Volume of the speaker in percent (1 = 100%, values: 0-1, default: 1, optional). /// Force SSML on supported platforms (default: true, optional). /// UID of the speaker. public string SpeakNative(string text, Model.Voice voice = null, float rate = 1f, float pitch = 1f, float volume = 1f, bool forceSSML = true) { Model.Wrapper wrapper = new Model.Wrapper(text, voice, rate, pitch, volume, forceSSML); SpeakNativeWithUID(wrapper); return wrapper.Uid; } /// Speaks a text with a given voice (native mode). /// Speak wrapper. public void SpeakNativeWithUID(Model.Wrapper wrapper) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"SpeakNativeWithUID called: {wrapper}", this); if (wrapper != null) { if (Util.Helper.isEditorMode) { #if UNITY_EDITOR speakNativeInEditor(wrapper); #endif } else { if (voiceProvider != null) { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty!", this); } else { BusyCount++; if (!voiceProvider.isSpeakNativeSupported) //add an AudioSource for providers without native support { if (wrapper.Source == null) { wrapper.Source = gameObject.AddComponent(); genericSources.Add(wrapper.Uid, wrapper.Source); } else { if (!providedSources.ContainsKey(wrapper.Uid)) providedSources.Add(wrapper.Uid, wrapper.Source); } wrapper.SpeakImmediately = true; //must always speak immediately } StartCoroutine(voiceProvider.SpeakNative(wrapper)); } } else { logVPIsNull(); } } } else { logWrapperIsNull(); } } /// Speaks a text with a given wrapper (native mode). /// Speak wrapper. /// UID of the speaker. public string SpeakNative(Model.Wrapper wrapper) { if (wrapper != null) { SpeakNativeWithUID(wrapper); return wrapper.Uid; } logWrapperIsNull(); return string.Empty; } /// Speaks a text with a given voice. /// Text to speak. /// AudioSource for the output (optional). /// Voice to speak (optional). /// Speak the text immediately (default: true). Only works if 'Source' is not null. /// Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional). /// Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional). /// Volume of the speaker in percent (1 = 100%, values: 0-1, default: 1, optional). /// Saves the generated audio to an output file (without extension, optional). /// Force SSML on supported platforms (default: true, optional). /// UID of the speaker. public string Speak(string text, AudioSource source = null, Model.Voice voice = null, bool speakImmediately = true, float rate = 1f, float pitch = 1f, float volume = 1f, string outputFile = "", bool forceSSML = true) { Model.Wrapper wrapper = new Model.Wrapper(text, voice, rate, pitch, volume, source, speakImmediately, outputFile, forceSSML); SpeakWithUID(wrapper); return wrapper.Uid; } /// Speaks a text with a given voice. /// Speak wrapper. public void SpeakWithUID(Model.Wrapper wrapper) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"SpeakWithUID called: {wrapper}", this); if (wrapper != null) { if (Util.Helper.isEditorMode) { #if UNITY_EDITOR speakNativeInEditor(wrapper); #endif } else { if (voiceProvider != null) { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty!", this); } else { BusyCount++; if (voiceProvider.isSpeakSupported) //audio file generation possible { if (wrapper.Source == null) { wrapper.Source = gameObject.AddComponent(); genericSources.Add(wrapper.Uid, wrapper.Source); if (string.IsNullOrEmpty(wrapper.OutputFile)) wrapper.SpeakImmediately = true; //must always speak immediately (since there is no AudioSource given and no output file wanted) } else { if (!providedSources.ContainsKey(wrapper.Uid)) providedSources.Add(wrapper.Uid, wrapper.Source); } wrapper.Source.mute = isMuted; //TODO activate in providers (waiting for it) //if (isPaused) // wrapper.Source.Pause(); } if (Caching && GlobalCache.Instance.Clips.ContainsKey(wrapper)) { if (Util.Config.DEBUG) Debug.Log($"Wrapper CACHED: {wrapper}", this); Util.Context.NumberOfCachedSpeeches++; StartCoroutine(voiceProvider.SpeakWithClip(wrapper, GlobalCache.Instance.GetClip(wrapper))); } else { if (Util.Config.DEBUG) Debug.Log($"Wrapper NOT cached: {wrapper}", this); Util.Context.NumberOfNonCachedSpeeches++; StartCoroutine(voiceProvider.Speak(wrapper)); } } } else { logVPIsNull(); } } } else { logWrapperIsNull(); } } /// Speaks a text with a given wrapper. /// Speak wrapper. /// UID of the speaker. public string Speak(Model.Wrapper wrapper) { if (wrapper != null) { SpeakWithUID(wrapper); return wrapper.Uid; } logWrapperIsNull(); return string.Empty; } /// Speaks and marks a text with a given wrapper. /// Speak wrapper. public void SpeakMarkedWordsWithUID(Model.Wrapper wrapper) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"SpeakMarkedWordsWithUID called: {wrapper}", this); if (voiceProvider != null) { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty!", this); } else { if (wrapper.Source == null || wrapper.Source.clip == null) { Debug.LogError("'wrapper.Source' must be a valid AudioSource with a clip! Use 'Speak()' before!", this); } else { BusyCount++; wrapper.SpeakImmediately = true; //TODO improve the detection for supported providers if (!Util.Helper.isMacOSPlatform && !Util.Helper.isWSABasedPlatform && !CustomMode) //prevent "double-speak" { wrapper.Volume = 0f; wrapper.Source.PlayDelayed(0.1f); } SpeakNativeWithUID(wrapper); } } } else { logVPIsNull(); } } /// Speaks and marks a text with a given voice and tracks the word position. /// UID of the speaker /// Text to speak. /// AudioSource for the output. /// Voice to speak (optional). /// Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional). /// Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional). /// Force SSML on supported platforms (default: true, optional). public void SpeakMarkedWordsWithUID(string uid, string text, AudioSource source, Model.Voice voice = null, float rate = 1f, float pitch = 1f, bool forceSSML = true) { SpeakMarkedWordsWithUID(new Model.Wrapper(uid, text, voice, rate, pitch, 0, source, true, "", forceSSML)); } // /// // /// Speaks a text with a given voice and tracks the word position. // /// // public static Guid SpeakMarkedWords(string text, AudioSource source = null, Voice voice = null, int rate = 1, int volume = 100) { // Guid result = Guid.NewGuid(); // // SpeakMarkedWordsWithUID(result, text, source, voice, rate, volume); // // return result; // } /// Generates an audio file from a given wrapper. /// Speak wrapper. /// UID of the generator. public string Generate(Model.Wrapper wrapper) { if (wrapper != null) { if (Util.Helper.isEditorMode) { #if UNITY_EDITOR generateInEditor(wrapper); #endif } else { if (voiceProvider != null) { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty! Can't generate audio file.", this); } else { if (string.IsNullOrEmpty(wrapper.OutputFile)) { Debug.LogWarning("'wrapper.OutputFile' is null or empty! Can't generate audio file.", this); } else { StartCoroutine(voiceProvider.Generate(wrapper)); } } return wrapper.Uid; } logVPIsNull(); } } else { logWrapperIsNull(); } return string.Empty; } /// Generates an audio file from a text with a given voice. /// Text to generate. /// Saves the generated audio to an output file (without extension). /// Voice to speak (optional). /// Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional). /// Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional). /// Volume of the speaker in percent (1 = 100%, values: 0-1, default: 1, optional). /// Force SSML on supported platforms (default: true, optional). /// UID of the generator. public string Generate(string text, string outputFile, Model.Voice voice = null, float rate = 1f, float pitch = 1f, float volume = 1f, bool forceSSML = true) { Model.Wrapper wrapper = new Model.Wrapper(text, voice, rate, pitch, volume, null, false, outputFile, forceSSML); return Generate(wrapper); } /// Silence all active TTS-voices. private void silence() { if (Util.Constants.DEV_DEBUG) Debug.LogWarning("Silence called", this); if (voiceProvider != null) { voiceProvider.Silence(); /* if (instance != null && voiceProvider.hasCoRoutines) StopAllCoroutines(); */ foreach (System.Collections.Generic.KeyValuePair source in genericSources.Where(source => source.Value != null)) { source.Value.Stop(); Destroy(source.Value, 0.1f); } genericSources.Clear(); foreach (System.Collections.Generic.KeyValuePair source in providedSources.Where(source => source.Value != null)) { source.Value.Stop(); } } else { providedSources.Clear(); if (!Common.Util.BaseHelper.isEditorMode) logVPIsNull(); } SpeechCount = 0; BusyCount = 0; } /// Silence all active TTS-voices (optional with a UID). /// UID of the speaker (optional) public void Silence(string uid = null) { if (Common.Util.BaseConstants.DEV_DEBUG) Debug.LogWarning($"Silence called: {uid}", this); if (voiceProvider != null) { if (string.IsNullOrEmpty(uid)) { silence(); } else { if (genericSources.ContainsKey(uid)) { if (genericSources.TryGetValue(uid, out AudioSource source)) { source.Stop(); genericSources.Remove(uid); } } else if (providedSources.ContainsKey(uid)) { if (providedSources.TryGetValue(uid, out AudioSource source)) { source.Stop(); providedSources.Remove(uid); } } else { voiceProvider.Silence(uid); } } } else { logVPIsNull(); } //SpeechCount--; } /// Pause all active TTS-voices (optional with a UID, only for 'Speak'-calls). /// UID of the speaker (optional) public void Pause(string uid = null) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"Pause called: {uid}", this); isPaused = true; #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR if (voiceProvider.GetType() == typeof(Provider.VoiceProviderIOS)) { float currentTime = Time.realtimeSinceStartup; if (lastTimePaused + delayPause < currentTime) { lastTimePaused = currentTime; ((Provider.VoiceProviderIOS)voiceProvider).Pause(); } else { Debug.LogWarning("'Pause' is called too fast - please slow down!", this); } } else { #endif if (voiceProvider != null) { if (!string.IsNullOrEmpty(uid)) { if (genericSources.ContainsKey(uid)) { if (genericSources.TryGetValue(uid, out AudioSource source)) source.Pause(); } else if (providedSources.ContainsKey(uid)) { if (providedSources.TryGetValue(uid, out AudioSource source)) source.Pause(); } else { Debug.Log($"No AudioSource for uid found: {uid}", this); } } else { foreach (System.Collections.Generic.KeyValuePair source in genericSources.Where(source => source.Value != null)) { source.Value.Pause(); } foreach (System.Collections.Generic.KeyValuePair source in providedSources.Where(source => source.Value != null)) { source.Value.Pause(); } } } else { logVPIsNull(); } #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR } #endif } /// Un-Pause all active TTS-voices (optional with a UID, only for 'Speak'-calls). /// UID of the speaker (optional) public void UnPause(string uid = null) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"UnPause called: {uid}", this); isPaused = false; #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR if (voiceProvider.GetType() == typeof(Provider.VoiceProviderIOS)) { float currentTime = Time.realtimeSinceStartup; if (lastTimePaused + delayPause < currentTime) { lastTimePaused = currentTime; if (currentWrapper != null) { currentWrapper.Text = currentTextToSpeak; SpeakNative(currentWrapper); //voiceProvider.SpeakNative(currentTextToSpeak); } } else { Debug.LogWarning("'UnPause' is called too fast - please slow down!", this); } } else { #endif if (voiceProvider != null) { if (!string.IsNullOrEmpty(uid)) { if (genericSources.ContainsKey(uid)) { if (genericSources.TryGetValue(uid, out AudioSource source)) source.UnPause(); } else if (providedSources.ContainsKey(uid)) { if (providedSources.TryGetValue(uid, out AudioSource source)) source.UnPause(); } else { Debug.Log($"No AudioSource for uid found: {uid}", this); } } else { foreach (System.Collections.Generic.KeyValuePair source in genericSources.Where(source => source.Value != null)) { source.Value.UnPause(); } foreach (System.Collections.Generic.KeyValuePair source in providedSources.Where(source => source.Value != null)) { source.Value.UnPause(); } } } else { logVPIsNull(); } #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR } #endif } /// Pause or unpause all active TTS-voices (optional with a UID, only for 'Speak'-calls). /// UID of the speaker (optional) public void PauseOrUnPause(string uid = null) { if (isPaused) { UnPause(uid); } else { Pause(uid); } } /// Mute all active TTS-voices (optional with a UID, only for 'Speak'-calls). /// UID of the speaker (optional) public void Mute(string uid = null) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"Mute called: {uid}", this); isMuted = true; if (voiceProvider != null) { if (!string.IsNullOrEmpty(uid)) { if (genericSources.ContainsKey(uid)) { if (genericSources.TryGetValue(uid, out AudioSource source)) source.mute = true; } else if (providedSources.ContainsKey(uid)) { if (providedSources.TryGetValue(uid, out AudioSource source)) source.mute = true; } else { Debug.Log($"No AudioSource for uid found: {uid}", this); } } else { foreach (System.Collections.Generic.KeyValuePair source in genericSources.Where(source => source.Value != null)) { source.Value.mute = true; } foreach (System.Collections.Generic.KeyValuePair source in providedSources.Where(source => source.Value != null)) { source.Value.mute = true; } } } else { logVPIsNull(); } } /// Un-mute all active TTS-voices (optional with a UID, only for 'Speak'-calls). /// UID of the speaker (optional) public void UnMute(string uid = null) { if (Util.Constants.DEV_DEBUG) Debug.LogWarning($"UnMute called: {uid}", this); isMuted = false; if (voiceProvider != null) { if (!string.IsNullOrEmpty(uid)) { if (genericSources.ContainsKey(uid)) { if (genericSources.TryGetValue(uid, out AudioSource source)) source.mute = false; } else if (providedSources.ContainsKey(uid)) { if (providedSources.TryGetValue(uid, out AudioSource source)) source.mute = false; } else { Debug.Log($"No AudioSource for uid found: {uid}", this); } } else { foreach (System.Collections.Generic.KeyValuePair source in genericSources.Where(source => source.Value != null)) { source.Value.mute = false; } foreach (System.Collections.Generic.KeyValuePair source in providedSources.Where(source => source.Value != null)) { source.Value.mute = false; } } } else { logVPIsNull(); } } /// Mute or unmute all active TTS-voices (optional with a UID, only for 'Speak'-calls). /// UID of the speaker (optional) public void MuteOrUnMute(string uid = null) { if (isMuted) { UnMute(uid); } else { Mute(uid); } } /// Reloads the provider. public void ReloadProvider() { Silence(); initProvider(); } /// Deletes all generated audio files. public void DeleteAudioFiles() { if (!Util.Helper.isWebPlatform) { string path = Application.persistentDataPath; #if !UNITY_WSA || UNITY_EDITOR if (deleteWorker?.IsAlive == true) { if (Util.Constants.DEV_DEBUG) Debug.Log("Killing worker", this); deleteWorker.Abort(); //TODO dangerous - find a better solution! } deleteWorker = new System.Threading.Thread(() => deleteAudioFiles(path)); deleteWorker.Start(); #else deleteAudioFiles(path); #endif } } #endregion #region Private methods private void deleteAudioFiles(string persistentDataPath) { try { System.Random rnd = new System.Random(); string filesToDelete = Util.Constants.AUDIOFILE_PREFIX + "*"; // + AudioFileExtension; string path = Util.Helper.isAndroidPlatform || Util.Helper.isWSABasedPlatform ? Util.Helper.ValidatePath(persistentDataPath) : Util.Config.AUDIOFILE_PATH; string[] fileList = System.IO.Directory.GetFiles(path, filesToDelete); foreach (string file in fileList) { try { #if !UNITY_WSA || UNITY_EDITOR if (Util.Helper.isWindowsPlatform /* && ii % 10 == 0 */) //only for Windows to prevent issues with AV { System.Threading.Thread.Sleep(rnd.Next(1200, 1800)); } #endif System.IO.File.Delete(file); } catch (System.Exception ex) { if (!Util.Helper.isEditor) Debug.LogWarning($"Could not delete the file '{file}': {ex}", this); } } } catch (System.Exception ex) { if (!Util.Helper.isEditor) Debug.LogWarning($"Could not scan the path for files: {ex}", this); } } private void initProvider() { unsubscribeEvents(); areVoicesReady = false; enforcedStandaloneTTS = false; bool useCustom = CustomProvider != null && CustomMode && CustomProvider.enabled; if (useCustom) { if (CustomProvider.isPlatformSupported) { subscribeCustomEvents(); voiceProvider = customVoiceProvider = CustomProvider; mainVoiceProvider = null; CustomProvider.Load(); //Debug.Log($"Load custom: {voiceProvider}"); } else { Debug.LogWarning("'Custom Provider' does not support the current platform!", this); useCustom = false; //if (!Util.Helper.isEditorMode) // CustomMode = false; } } if (!useCustom) { unsubscribeCustomEvents(); customVoiceProvider = null; initOSProvider(); subscribeEvents(); voiceProvider?.Load(); onProviderChange(); } } private void initOSProvider() { if (!Util.Helper.isMacOSEditor && !Util.Helper.isLinuxEditor && Util.Helper.isWindowsPlatform && !eSpeakMode || Util.Helper.isWindowsEditor && Util.Config.ENFORCE_STANDALONE_TTS && !eSpeakMode) { enforcedStandaloneTTS = !Util.Helper.isWindowsPlatform && Util.Helper.isWindowsEditor && Util.Config.ENFORCE_STANDALONE_TTS; #if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN voiceProvider = mainVoiceProvider = Provider.VoiceProviderWindows.Instance; #endif } else if (!Util.Helper.isWindowsEditor && !Util.Helper.isLinuxEditor && Util.Helper.isMacOSPlatform && !eSpeakMode || Util.Helper.isMacOSEditor && Util.Config.ENFORCE_STANDALONE_TTS && !eSpeakMode) { enforcedStandaloneTTS = !Util.Helper.isMacOSPlatform && Util.Helper.isMacOSEditor && Util.Config.ENFORCE_STANDALONE_TTS; #if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX //|| CT_DEVELOP voiceProvider = mainVoiceProvider = Provider.VoiceProviderMacOS.Instance; #endif } #if UNITY_STANDALONE || UNITY_EDITOR else if (eSpeakMode && Provider.VoiceProviderLinux.isSupported) { voiceProvider = mainVoiceProvider = Provider.VoiceProviderLinux.Instance; } #endif else if (Util.Helper.isAndroidPlatform) { #if UNITY_ANDROID || UNITY_EDITOR voiceProvider = mainVoiceProvider = Provider.VoiceProviderAndroid.Instance; #endif } else if (Util.Helper.isIOSBasedPlatform) { #if UNITY_IOS || UNITY_TVOS || UNITY_EDITOR voiceProvider = mainVoiceProvider = Provider.VoiceProviderIOS.Instance; #endif } #if (UNITY_WSA && !UNITY_EDITOR) && ENABLE_WINMD_SUPPORT //|| CT_DEVELOP else if (Util.Helper.isWSABasedPlatform) { voiceProvider = mainVoiceProvider = Provider.VoiceProviderWSA.Instance; } #endif else { Debug.LogError("No valid TTS provider found!", this); voiceProvider = mainVoiceProvider = null; //voiceProvider = new Provider.VoiceProviderLinux(); // always add a default provider } //Debug.Log("VP: " + voiceProvider); //voiceProvider?.Load(); } private void logWrapperIsNull() { const string errorMessage = "'wrapper' is null!"; onErrorInfo(null, errorMessage); Debug.LogError(errorMessage, this); } private void logVPIsNull() { string errorMessage = "'voiceProvider' is null!" + System.Environment.NewLine + "Did you add the 'RTVoice'-prefab to the current scene?"; onErrorInfo(null, errorMessage); if (!loggedVPIsNull && !Common.Util.BaseHelper.isEditorMode) { Debug.LogWarning(errorMessage, this); loggedVPIsNull = true; } } private void subscribeCustomEvents() { if (CustomProvider != null) { CustomProvider.isActive = true; CustomProvider.OnVoicesReady += onVoicesReady; CustomProvider.OnSpeakStart += onSpeakStart; CustomProvider.OnSpeakComplete += onSpeakComplete; CustomProvider.OnSpeakCurrentWord += onSpeakCurrentWord; CustomProvider.OnSpeakCurrentPhoneme += onSpeakCurrentPhoneme; CustomProvider.OnSpeakCurrentViseme += onSpeakCurrentViseme; CustomProvider.OnSpeakAudioGenerationStart += onSpeakAudioGenerationStart; CustomProvider.OnSpeakAudioGenerationComplete += onSpeakAudioGenerationComplete; CustomProvider.OnErrorInfo += onErrorInfo; } } private void unsubscribeCustomEvents() { if (CustomProvider != null) { CustomProvider.isActive = false; CustomProvider.OnVoicesReady -= onVoicesReady; CustomProvider.OnSpeakStart -= onSpeakStart; CustomProvider.OnSpeakComplete -= onSpeakComplete; CustomProvider.OnSpeakCurrentWord -= onSpeakCurrentWord; CustomProvider.OnSpeakCurrentPhoneme -= onSpeakCurrentPhoneme; CustomProvider.OnSpeakCurrentViseme -= onSpeakCurrentViseme; CustomProvider.OnSpeakAudioGenerationStart -= onSpeakAudioGenerationStart; CustomProvider.OnSpeakAudioGenerationComplete -= onSpeakAudioGenerationComplete; CustomProvider.OnErrorInfo -= onErrorInfo; } } private void subscribeEvents() { if (mainVoiceProvider != null) { mainVoiceProvider.OnVoicesReady += onVoicesReady; mainVoiceProvider.OnSpeakStart += onSpeakStart; mainVoiceProvider.OnSpeakComplete += onSpeakComplete; mainVoiceProvider.OnSpeakCurrentWord += onSpeakCurrentWord; mainVoiceProvider.OnSpeakCurrentPhoneme += onSpeakCurrentPhoneme; mainVoiceProvider.OnSpeakCurrentViseme += onSpeakCurrentViseme; mainVoiceProvider.OnSpeakAudioGenerationStart += onSpeakAudioGenerationStart; mainVoiceProvider.OnSpeakAudioGenerationComplete += onSpeakAudioGenerationComplete; mainVoiceProvider.OnErrorInfo += onErrorInfo; } if (customVoiceProvider != null) { customVoiceProvider.OnVoicesReady += onVoicesReady; customVoiceProvider.OnSpeakStart += onSpeakStart; customVoiceProvider.OnSpeakComplete += onSpeakComplete; customVoiceProvider.OnSpeakCurrentWord += onSpeakCurrentWord; customVoiceProvider.OnSpeakCurrentPhoneme += onSpeakCurrentPhoneme; customVoiceProvider.OnSpeakCurrentViseme += onSpeakCurrentViseme; customVoiceProvider.OnSpeakAudioGenerationStart += onSpeakAudioGenerationStart; customVoiceProvider.OnSpeakAudioGenerationComplete += onSpeakAudioGenerationComplete; customVoiceProvider.OnErrorInfo += onErrorInfo; } } private void unsubscribeEvents() { if (mainVoiceProvider != null) { mainVoiceProvider.OnVoicesReady -= onVoicesReady; mainVoiceProvider.OnSpeakStart -= onSpeakStart; mainVoiceProvider.OnSpeakComplete -= onSpeakComplete; mainVoiceProvider.OnSpeakCurrentWord -= onSpeakCurrentWord; mainVoiceProvider.OnSpeakCurrentPhoneme -= onSpeakCurrentPhoneme; mainVoiceProvider.OnSpeakCurrentViseme -= onSpeakCurrentViseme; mainVoiceProvider.OnSpeakAudioGenerationStart -= onSpeakAudioGenerationStart; mainVoiceProvider.OnSpeakAudioGenerationComplete -= onSpeakAudioGenerationComplete; mainVoiceProvider.OnErrorInfo -= onErrorInfo; } if (customVoiceProvider != null) { customVoiceProvider.OnVoicesReady -= onVoicesReady; customVoiceProvider.OnSpeakStart -= onSpeakStart; customVoiceProvider.OnSpeakComplete -= onSpeakComplete; customVoiceProvider.OnSpeakCurrentWord -= onSpeakCurrentWord; customVoiceProvider.OnSpeakCurrentPhoneme -= onSpeakCurrentPhoneme; customVoiceProvider.OnSpeakCurrentViseme -= onSpeakCurrentViseme; customVoiceProvider.OnSpeakAudioGenerationStart -= onSpeakAudioGenerationStart; customVoiceProvider.OnSpeakAudioGenerationComplete -= onSpeakAudioGenerationComplete; customVoiceProvider.OnErrorInfo -= onErrorInfo; } } #endregion #region Event-trigger methods private void onVoicesReady() { areVoicesReady = true; if (!Util.Helper.isEditorMode) OnReady?.Invoke(); OnVoicesReady?.Invoke(); } private void onProviderChange() { if (!Util.Helper.isEditorMode) OnProviderChanged?.Invoke(voiceProvider.GetType().ToString()); OnProviderChange?.Invoke(voiceProvider.GetType().ToString()); } private void onSpeakStart(Model.Wrapper wrapper) { if (!Util.Helper.isEditorMode) OnSpeakStarted?.Invoke(wrapper?.Uid); OnSpeakStart?.Invoke(wrapper); SpeechCount++; } private void onSpeakComplete(Model.Wrapper wrapper) { #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR currentTextToSpeak = null; currentWrapper = null; #endif if (!Util.Helper.isEditorMode) OnSpeakCompleted?.Invoke(wrapper?.Uid); OnSpeakComplete?.Invoke(wrapper); SpeechCount--; BusyCount--; Util.Context.NumberOfSpeeches++; //if (wrapper.isNative) Util.Context.TotalSpeechLength += wrapper.SpeechTime; Util.Context.NumberOfCharacters += wrapper.Text.Length; } private void onSpeakCurrentWord(Model.Wrapper wrapper, string[] speechTextArray, int wordIndex) { #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR if (voiceProvider.GetType() == typeof(Provider.VoiceProviderIOS)) { //currentTextToSpeak = string.Join(" ", speechTextArray, wordIndex + 1, speechTextArray.Length - wordIndex - 1); currentTextToSpeak = string.Join(" ", speechTextArray, wordIndex, speechTextArray.Length - wordIndex); currentWrapper = wrapper; currentWrapper.isPartial = true; } #endif OnSpeakCurrentWord?.Invoke(wrapper, speechTextArray, wordIndex); } private void onSpeakCurrentPhoneme(Model.Wrapper wrapper, string phoneme) { OnSpeakCurrentPhoneme?.Invoke(wrapper, phoneme); } private void onSpeakCurrentViseme(Model.Wrapper wrapper, string viseme) { OnSpeakCurrentViseme?.Invoke(wrapper, viseme); } private void onSpeakAudioGenerationStart(Model.Wrapper wrapper) { OnSpeakAudioGenerationStart?.Invoke(wrapper); } private void onSpeakAudioGenerationComplete(Model.Wrapper wrapper) { OnSpeakAudioGenerationComplete?.Invoke(wrapper); Util.Context.NumberOfAudioFiles++; Util.Context.TotalSpeechLength += wrapper.SpeechTime; Util.Context.NumberOfCharacters += wrapper.Text.Length; } private void onErrorInfo(Model.Wrapper wrapper, string errorInfo) { if (!Util.Helper.isEditorMode) OnError?.Invoke(wrapper?.Uid, errorInfo); OnErrorInfo?.Invoke(wrapper, errorInfo); } #endregion #region Editor-only methods #if UNITY_EDITOR private void speakNativeInEditor(Model.Wrapper wrapper) { if (Util.Helper.isEditorMode) { if (voiceProvider != null) { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty!", this); } else { System.Threading.Thread worker = new System.Threading.Thread(() => voiceProvider.SpeakNativeInEditor(wrapper)); worker.Start(); } //return wrapper.Uid; } else { logVPIsNull(); } } else { Debug.LogWarning("'SpeakNativeInEditor()' works only inside the Unity Editor!", this); } //return string.Empty; } private void generateInEditor(Model.Wrapper wrapper) { if (Util.Helper.isEditorMode) { if (voiceProvider != null) { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty!", this); } else { System.Threading.Thread worker = new System.Threading.Thread(() => voiceProvider.GenerateInEditor(wrapper)); worker.Start(); } //return wrapper.Uid; } logVPIsNull(); } else { Debug.LogWarning("'GenerateInEditor()' works only inside the Unity Editor!", this); } //return string.Empty; } #endif #endregion #region iOS #if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR /// Sets all voices from iOS. /// All voices from iOS. public void SetVoices(string voices) { Provider.VoiceProviderIOS.SetVoices(voices); } /// The current spoken word from iOS. /// Current spoken word from iOS. public void WordSpoken(string word) { Provider.VoiceProviderIOS.WordSpoken(); } /// Sets the state from iOS. /// State from iOS. public void SetState(string state) { Provider.VoiceProviderIOS.SetState(state); } #endif #endregion } } // © 2015-2020 crosstales LLC (https://www.crosstales.com)