ISAP/Assets/Plugins/crosstales/RTVoice/Scripts/Tool/Paralanguage.cs

342 lines
9.5 KiB
C#

using UnityEngine;
using System.Collections;
using System.Linq;
namespace Crosstales.RTVoice.Tool
{
/// <summary>Para-language simulator with audio files.</summary>
//[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<int, string> stack = new System.Collections.Generic.SortedDictionary<int, string>();
private readonly System.Collections.Generic.IDictionary<string, AudioClip> clipDict = new System.Collections.Generic.Dictionary<string, AudioClip>();
private AudioSource audioSource;
private bool next;
#endregion
#region Events
[Header("Events")] public ParalanguageStartEvent OnStarted;
public ParalanguageCompleteEvent OnCompleted;
/// <summary>An event triggered whenever a Paralanguage 'Speak' is started.</summary>
public event ParalanguageStart OnParalanguageStart;
/// <summary>An event triggered whenever a Paralanguage 'Speak' is completed.</summary>
public event ParalanguageComplete OnParalanguageComplete;
#endregion
#region Properties
/// <summary>Text to speak.</summary>
public string Text
{
get => text;
set => text = value;
}
/// <summary>Voices for the speech.</summary>
public Model.VoiceAlias Voices
{
get => voices;
set => voices = value;
}
/// <summary>Speak mode.</summary>
public Model.Enum.SpeakMode Mode
{
get => mode;
set => mode = value;
}
/// <summary>Audio clips to play.</summary>
public AudioClip[] Clips
{
get => clips;
set => clips = value;
}
/// <summary>Speech rate of the speaker in percent (range: 0-3).</summary>
public float Rate
{
get => rate;
set => rate = Mathf.Clamp(value, 0, 3);
}
/// <summary>Speech pitch of the speaker in percent (range: 0-2).</summary>
public float Pitch
{
get => pitch;
set => pitch = Mathf.Clamp(value, 0, 2);
}
/// <summary>Volume of the speaker in percent (range: 0-1).</summary>
public float Volume
{
get => volume;
set => volume = Mathf.Clamp01(value);
}
/// <summary>Enable speaking of the text on start.</summary>
public bool PlayOnStart
{
get => playOnStart;
set => playOnStart = value;
}
/// <summary>Delay until the speech for this text starts.</summary>
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>();
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
/// <summary>Speak the text.</summary>
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());
}
/// <summary>Silence the speech.</summary>
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<int, string> 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)