using UnityEngine; namespace Crosstales.RTVoice.Tool { /// Allows to speak text files. [ExecuteInEditMode] [HelpURL("https://www.crosstales.com/media/data/assets/rtvoice/api/class_crosstales_1_1_r_t_voice_1_1_tool_1_1_text_file_speaker.html")] public class TextFileSpeaker : MonoBehaviour { #region Variables [UnityEngine.Serialization.FormerlySerializedAsAttribute("TextFiles")] [Tooltip("Text files to speak."), SerializeField] private TextAsset[] textFiles; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Voices")] [Tooltip("Voices for the speech."), SerializeField] private Model.VoiceAlias voices; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Mode")] [Tooltip("Speak mode (default: 'Speak')."), SerializeField] private Model.Enum.SpeakMode mode = Model.Enum.SpeakMode.Speak; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Source")] [Header("Optional Settings"), Tooltip("AudioSource for the output (optional)."), SerializeField] private AudioSource source; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Rate")] [Tooltip("Speech rate of the speaker in percent (1 = 100%, default: 1, optional)."), Range(0f, 3f), SerializeField] private float rate = 1f; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Pitch")] [Tooltip("Speech pitch of the speaker in percent (1 = 100%, default: 1, optional, mobile only)."), Range(0f, 2f), SerializeField] private float pitch = 1f; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Volume")] [Tooltip("Volume of the speaker in percent (1 = 100%, default: 1, optional, Windows only)."), Range(0f, 1f), SerializeField] private float volume = 1f; [UnityEngine.Serialization.FormerlySerializedAsAttribute("PlayOnStart")] [Header("Behaviour Settings"), Tooltip("Enable speaking of a random text file on start (default: false)."), SerializeField] private bool playOnStart; [UnityEngine.Serialization.FormerlySerializedAsAttribute("PlayAllOnStart")] [Tooltip("Enable speaking of a random text file on start (default: false)."), SerializeField] private bool playAllOnStart; [UnityEngine.Serialization.FormerlySerializedAsAttribute("SpeakRandom")] [Tooltip("Speaks the text files in random order (default: false)."), SerializeField] private bool speakRandom; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Delay")] [Tooltip("Delay until the speech for this text starts (default: 0.1)."), SerializeField] private float delay = 0.1f; private string[] texts; private string[] randomTexts; private int textIndex = -1; private int randomTextIndex = -1; //private Voice voice; private static readonly System.Random rnd = new System.Random(); private string uid = string.Empty; private bool played; private bool playAll; private float lastSpeaktime = float.MinValue; private int lastNumberOfTextfiles = -1; #endregion #region Properties /// Text files to speak. public TextAsset[] TextFiles { get => textFiles; set => textFiles = value; } /// Voices for the speech. public Model.VoiceAlias Voices { get => voices; set => voices = value; } /// Speak mode. public Model.Enum.SpeakMode Mode { get => mode; set => mode = value; } /// AudioSource for the output (optional). public AudioSource Source { get => source; set => source = value; } /// Speech rate of the speaker in percent (range: 0-3). public float Rate { get => rate; set => rate = Mathf.Clamp(value, 0, 3); } /// Speech pitch of the speaker in percent (range: 0-2). public float Pitch { get => pitch; set => pitch = Mathf.Clamp(value, 0, 2); } /// Volume of the speaker in percent (range: 0-1). public float Volume { get => volume; set => volume = Mathf.Clamp01(value); } /// Enable speaking of the text on start. public bool PlayOnStart { get => playOnStart; set => playOnStart = value; } /// /// Enable speaking of a all random text files on start (default: false). /// NOTE: this can only be stopped with the "StopAll"-method /// public bool PlayAllOnStart { get => playAllOnStart; set => playAllOnStart = value; } /// Speaks the text files in random order. public bool SpeakRandom { get => speakRandom; set => speakRandom = value; } /// Delay until the speech for this text starts. public float Delay { get => delay; set => delay = Mathf.Abs(value); } #endregion #region Events [Header("Events")] public TextFileSpeakerStartEvent OnStarted; public TextFileSpeakerCompleteEvent OnCompleted; /// An event triggered whenever a TextFileSpeaker 'Speak' is started. public event TextFileSpeakerStart OnTextFileSpeakerStart; /// An event triggered whenever a TextFileSpeaker 'Speak' is completed. public event TextFileSpeakerComplete OnTextFileSpeakerComplete; #endregion #region MonoBehaviour methods private void Start() { Speaker.Instance.OnVoicesReady += onVoicesReady; Speaker.Instance.OnSpeakStart += onSpeakStart; Speaker.Instance.OnSpeakComplete += onSpeakComplete; Reload(); play(); } private void OnDestroy() { if (!Util.Helper.isEditorMode && Speaker.Instance != null) { Speaker.Instance.OnVoicesReady -= onVoicesReady; Speaker.Instance.OnSpeakStart -= onSpeakStart; Speaker.Instance.OnSpeakComplete -= onSpeakComplete; } } private void Update() { if (textFiles.Length != lastNumberOfTextfiles) Reload(); } private void OnValidate() { if (delay < 0f) delay = 0f; rate = Mathf.Clamp(rate, 0f, 3f); pitch = Mathf.Clamp(pitch, 0f, 2f); volume = Mathf.Clamp01(volume); } #endregion #region Public methods /// Speaks all texts until StopAll is called. public void SpeakAll() { playAll = true; Next(); } /// Stops speaking all texts. public void StopAll() { playAll = false; Silence(); } /// Speaks the next text (main use for UI). public void Next() { Next(speakRandom); } /// Speaks the next text. /// Speak a random text public void Next(bool random) { int index; if (random) { if (randomTextIndex > -1 && randomTextIndex + 1 < randomTexts.Length) { randomTextIndex++; } else { randomTextIndex = 0; } index = randomTextIndex; } else { if (textIndex > -1 && textIndex + 1 < texts.Length) { textIndex++; } else { textIndex = 0; } index = textIndex; } SpeakText(index, random); } /// Speaks the previous text (main use for UI). public void Previous() { Previous(speakRandom); } /// Speaks the previous text. /// Speak a random text public void Previous(bool random) { int index; if (random) { if (randomTextIndex > 0 && randomTextIndex < randomTexts.Length) { randomTextIndex--; } else { randomTextIndex = randomTexts.Length - 1; } index = randomTextIndex; } else { if (textIndex > 0 && textIndex < texts.Length) { textIndex--; } else { textIndex = texts.Length - 1; } index = textIndex; } SpeakText(index, random); } /// Speaks a text (main use for UI). public void Speak() { Next(); } /// Speaks a text with an optional index. /// Index of the text (default: -1 (random), optional). /// Speak a random text (default: false, optional) /// UID of the speaker. public string SpeakText(int index = -1, bool random = false) { float currentTime = Time.realtimeSinceStartup; if (lastSpeaktime + Util.Constants.SPEAK_CALL_SPEED < currentTime) { lastSpeaktime = currentTime; Silence(); string result = string.Empty; if (texts.Length > 0) { if (random) { if (index < 0) { result = speak(randomTexts[rnd.Next(randomTexts.Length)]); } else { if (index < texts.Length) { result = speak(randomTexts[index]); } else { Debug.LogWarning("Text file index is out of bounds: " + index + " - maximal index is: " + (randomTexts.Length - 1), this); result = speak(randomTexts[randomTexts.Length - 1]); } } } else { if (index < 0) { result = speak(texts[rnd.Next(texts.Length)]); } else { if (index < texts.Length) { result = speak(texts[index]); } else { Debug.LogWarning("Text file index is out of bounds: " + index + " - maximal index is: " + (texts.Length - 1), this); result = speak(texts[texts.Length - 1]); } } } } else { Debug.LogError("No text files added - speak cancelled!", this); } uid = result; } else { Debug.LogWarning("'SpeakText' called too fast - please slow down!", this); } return uid; } /// Silence the speech. public void Silence() { if (Util.Helper.isEditorMode) { Speaker.Instance.Silence(); } else { if (!string.IsNullOrEmpty(uid)) Speaker.Instance.Silence(uid); } } /// Reloads all text files (e.g. when new text files were added during runtime). public void Reload() { if (textFiles.Length > 0) { texts = new string[textFiles.Length]; randomTexts = new string[textFiles.Length]; lastNumberOfTextfiles = textFiles.Length; for (int ii = 0; ii < textFiles.Length; ii++) { if (textFiles[ii] != null) { randomTexts[ii] = texts[ii] = textFiles[ii].text; } else { randomTexts[ii] = texts[ii] = string.Empty; } } randomTexts.CTShuffle(); textIndex = -1; randomTextIndex = -1; } } #endregion #region Private methods private void play() { if (!Util.Helper.isEditorMode) { if (!played && Speaker.Instance.Voices.Count > 0) { played = true; if (playOnStart) { Invoke(nameof(Next), delay); } else if (playAllOnStart) { Invoke(nameof(SpeakAll), delay); } } } } private string speak(string text) { return mode == Model.Enum.SpeakMode.Speak ? Speaker.Instance.Speak(text, source, voices.Voice, true, rate, pitch, volume) : Speaker.Instance.SpeakNative(text, voices.Voice, rate, pitch, volume); } #endregion #region Callbacks private void onVoicesReady() { play(); } #endregion #region Event-trigger methods private void onSpeakStart(Crosstales.RTVoice.Model.Wrapper wrapper) { if (wrapper.Uid.Equals(uid)) { if (!Util.Helper.isEditorMode) OnStarted?.Invoke(); OnTextFileSpeakerStart?.Invoke(); } } private void onSpeakComplete(Model.Wrapper wrapper) { if (wrapper.Uid.Equals(uid)) { if (!Util.Helper.isEditorMode) OnCompleted?.Invoke(); OnTextFileSpeakerComplete?.Invoke(); if (playAll) Invoke(nameof(Next), delay); } } #endregion } } // © 2016-2020 crosstales LLC (https://www.crosstales.com)