using UnityEngine; using System.Collections; using System.Linq; namespace Crosstales.RTVoice.Tool { /// Para-language simulator with audio files. //[ExecuteInEditMode] [RequireComponent(typeof(AudioSource))] [HelpURL("https://www.crosstales.com/media/data/assets/rtvoice/api/class_crosstales_1_1_r_t_voice_1_1_tool_1_1_paralanguage.html")] public class Paralanguage : MonoBehaviour { #region Variables [UnityEngine.Serialization.FormerlySerializedAsAttribute("Text")] [Tooltip("Text to speak."), TextArea(3, 15), SerializeField] private string text = string.Empty; [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("Clips")] [Tooltip("Audio clips to play."), SerializeField] private AudioClip[] clips; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Rate")] [Header("Optional Settings"), 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)."), Range(0f, 2f), SerializeField] private float pitch = 1f; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Volume")] [Tooltip("Volume of the speaker in percent (1 = 100%, default: 1, optional)."), Range(0f, 1f), SerializeField] private float volume = 1f; [UnityEngine.Serialization.FormerlySerializedAsAttribute("PlayOnStart")] [Header("Behaviour Settings"), Tooltip("Enable speaking of the text on start (default: false)."), SerializeField] private bool playOnStart; [UnityEngine.Serialization.FormerlySerializedAsAttribute("Delay")] [Tooltip("Delay until the speech for this text starts (default: 0.1"), SerializeField] private float delay = 0.1f; private static readonly System.Text.RegularExpressions.Regex splitRegex = new System.Text.RegularExpressions.Regex(@"#.*?#"); private string uid; private bool played; private readonly System.Collections.Generic.IDictionary stack = new System.Collections.Generic.SortedDictionary(); private readonly System.Collections.Generic.IDictionary clipDict = new System.Collections.Generic.Dictionary(); private AudioSource audioSource; private bool next; #endregion #region Events [Header("Events")] public ParalanguageStartEvent OnStarted; public ParalanguageCompleteEvent OnCompleted; /// An event triggered whenever a Paralanguage 'Speak' is started. public event ParalanguageStart OnParalanguageStart; /// An event triggered whenever a Paralanguage 'Speak' is completed. public event ParalanguageComplete OnParalanguageComplete; #endregion #region Properties /// Text to speak. public string Text { get => text; set => text = 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; } /// Audio clips to play. public AudioClip[] Clips { get => clips; set => clips = 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; } /// Delay until the speech for this text starts. public float Delay { get => delay; set => delay = Mathf.Abs(value); } #endregion #region MonoBehaviour methods private void OnDestroy() { if (!Util.Helper.isEditorMode && Speaker.Instance != null) { Speaker.Instance.OnVoicesReady -= onVoicesReady; Speaker.Instance.OnSpeakComplete -= onSpeakComplete; } } private void Awake() { audioSource = GetComponent(); audioSource.playOnAwake = false; audioSource.loop = false; audioSource.Stop(); //always stop the AudioSource at startup } private void Start() { Speaker.Instance.OnVoicesReady += onVoicesReady; Speaker.Instance.OnSpeakComplete += onSpeakComplete; play(); } 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 /// Speak the text. public void Speak() { Silence(); stack.Clear(); clipDict.Clear(); foreach (AudioClip clip in clips) { clipDict.Add("#" + clip.name + "#", clip); } string[] speechParts = splitRegex.Split(text).Where(s => s != string.Empty).ToArray(); System.Text.RegularExpressions.MatchCollection mc = splitRegex.Matches(text); int index = 0; foreach (System.Text.RegularExpressions.Match match in mc) { //Debug.Log("MATCH: '" + match + "' - " + Text.IndexOf(match.ToString(), index)); stack.Add(index = text.CTIndexOf(match.ToString(), index), match.ToString()); index++; } index = 0; foreach (string speech in speechParts) { //Debug.Log("PART: '" + speech + "' - " + Text.IndexOf(speech, index)); stack.Add(index = text.CTIndexOf(speech, index), speech); index++; } StartCoroutine(processStack()); } /// Silence the speech. public void Silence() { StopAllCoroutines(); if (Util.Helper.isEditorMode) { Speaker.Instance.Silence(); } else { if (!string.IsNullOrEmpty(uid)) Speaker.Instance.Silence(uid); } } #endregion #region Private methods private IEnumerator processStack() { onStart(); foreach (System.Collections.Generic.KeyValuePair kvp in stack) { if (kvp.Value.CTStartsWith("#")) { clipDict.TryGetValue(kvp.Value, out AudioClip clip); if (clipDict.TryGetValue(kvp.Value, out clip)) { audioSource.clip = clip; audioSource.Play(); do { yield return null; } while (audioSource.isPlaying); } else { Debug.LogWarning("Clip not found: " + kvp.Value, this); } } else { next = false; uid = mode == Model.Enum.SpeakMode.Speak ? Speaker.Instance.Speak(kvp.Value, audioSource, voices.Voice, true, rate, pitch, volume) : Speaker.Instance.SpeakNative(kvp.Value, voices.Voice, rate, pitch, volume); do { yield return null; } while (!next); } } onComplete(); } private void play() { if (playOnStart && !played && Speaker.Instance.Voices.Count > 0) { played = true; Invoke(nameof(Speak), delay); } } #endregion #region Callbacks private void onVoicesReady() { play(); } private void onSpeakComplete(Model.Wrapper wrapper) { if (wrapper.Uid.Equals(uid)) { next = true; } } #endregion #region Event-trigger methods private void onStart() { if (Util.Config.DEBUG) Debug.Log("onStart", this); if (!Util.Helper.isEditorMode) OnStarted?.Invoke(); OnParalanguageStart?.Invoke(); } private void onComplete() { if (Util.Config.DEBUG) Debug.Log("onComplete", this); if (!Util.Helper.isEditorMode) OnCompleted?.Invoke(); OnParalanguageComplete?.Invoke(); } #endregion } } // © 2018-2020 crosstales LLC (https://www.crosstales.com)