2268 lines
77 KiB
C#
2268 lines
77 KiB
C#
using UnityEngine;
|
|
using System.Linq;
|
|
using UnityEngine.Serialization;
|
|
|
|
namespace Crosstales.RTVoice
|
|
{
|
|
/// <summary>Main component of RT-Voice.</summary>
|
|
[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<Speaker>
|
|
{
|
|
#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;
|
|
|
|
/*
|
|
/// <summary>Files to delete at the application end.</summary>
|
|
public static readonly System.Collections.Generic.List<string> FilesToDelete = new System.Collections.Generic.List<string>();
|
|
*/
|
|
|
|
private readonly System.Collections.Generic.Dictionary<string, AudioSource> removeSources = new System.Collections.Generic.Dictionary<string, AudioSource>();
|
|
|
|
private float cleanUpTimer;
|
|
|
|
private Provider.IVoiceProvider voiceProvider;
|
|
private Provider.MainVoiceProvider mainVoiceProvider;
|
|
private Provider.BaseCustomVoiceProvider customVoiceProvider;
|
|
private readonly System.Collections.Generic.Dictionary<string, AudioSource> genericSources = new System.Collections.Generic.Dictionary<string, AudioSource>();
|
|
private readonly System.Collections.Generic.Dictionary<string, AudioSource> providedSources = new System.Collections.Generic.Dictionary<string, AudioSource>();
|
|
|
|
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
|
|
|
|
/// <summary>Custom provider for RT-Voice.</summary>
|
|
public Provider.BaseCustomVoiceProvider CustomProvider
|
|
{
|
|
get => customProvider;
|
|
set
|
|
{
|
|
if (customProvider == value) return;
|
|
|
|
customProvider = value;
|
|
|
|
ReloadProvider();
|
|
}
|
|
}
|
|
|
|
/// <summary>Enables or disables the custom provider.</summary>
|
|
public bool CustomMode
|
|
{
|
|
get => customMode;
|
|
set
|
|
{
|
|
if (customMode == value) return;
|
|
|
|
customMode = value;
|
|
|
|
ReloadProvider();
|
|
}
|
|
}
|
|
|
|
/// <summary>Enable or disable eSpeak for standalone platforms.</summary>
|
|
public bool ESpeakMode
|
|
{
|
|
get => eSpeakMode;
|
|
set
|
|
{
|
|
if (eSpeakMode == value) return;
|
|
|
|
eSpeakMode = value;
|
|
|
|
ReloadProvider();
|
|
}
|
|
}
|
|
|
|
/// <summary>eSpeak application name/path.</summary>
|
|
public string ESpeakApplication
|
|
{
|
|
get => eSpeakApplication;
|
|
set => eSpeakApplication = value;
|
|
}
|
|
|
|
/// <summary>eSpeak application data path.</summary>
|
|
public string ESpeakDataPath
|
|
{
|
|
get => eSpeakDataPath;
|
|
set => eSpeakDataPath = value;
|
|
}
|
|
|
|
/// <summary>Active modifier for all eSpeak voices.</summary>
|
|
public Model.Enum.ESpeakModifiers ESpeakModifier
|
|
{
|
|
get => eSpeakModifier;
|
|
set => eSpeakModifier = value;
|
|
}
|
|
|
|
/// <summary>Active speech engine under Android.</summary>
|
|
public string AndroidEngine
|
|
{
|
|
get => androidEngine;
|
|
set
|
|
{
|
|
if (androidEngine == value) return;
|
|
|
|
androidEngine = value;
|
|
|
|
ReloadProvider();
|
|
}
|
|
}
|
|
|
|
/// <summary>Automatically clear tags from speeches depending on the capabilities of the current TTS-system.</summary>
|
|
public bool AutoClearTags
|
|
{
|
|
get => autoClearTags;
|
|
set => autoClearTags = value;
|
|
}
|
|
|
|
/// <summary>Enable or disable the caching of generated speeches.</summary>
|
|
public bool Caching
|
|
{
|
|
get => caching;
|
|
set => caching = value;
|
|
}
|
|
|
|
/// <summary>Silence any speeches if this component gets disabled.</summary>
|
|
public bool SilenceOnDisable
|
|
{
|
|
get => silenceOnDisable;
|
|
set => silenceOnDisable = value;
|
|
}
|
|
|
|
/// <summary>Silence any speeches if the application loses the focus.</summary>
|
|
public bool SilenceOnFocusLost
|
|
{
|
|
get => silenceOnFocusLost;
|
|
set => silenceOnFocusLost = value;
|
|
}
|
|
|
|
/// <summary>Starts and stops the Speaker depending on the focus and running state.</summary>
|
|
public bool HandleFocus
|
|
{
|
|
get => handleFocus;
|
|
set => handleFocus = value;
|
|
}
|
|
|
|
/*
|
|
/// <summary>Don't destroy gameobject during scene switches.</summary>
|
|
public bool DontDestroy
|
|
{
|
|
get => dontDestroy;
|
|
set => dontDestroy = value;
|
|
}
|
|
*/
|
|
/// <summary>Number of active speeches.</summary>
|
|
public int SpeechCount
|
|
{
|
|
get => speechCount;
|
|
private set => speechCount = value < 0 ? 0 : value;
|
|
}
|
|
|
|
/// <summary>Number of active calls.</summary>
|
|
public int BusyCount
|
|
{
|
|
get => busyCount;
|
|
private set => busyCount = value < 0 ? 0 : value;
|
|
}
|
|
|
|
/// <summary>Are all voices ready to speak?</summary>
|
|
public bool areVoicesReady { get; private set; }
|
|
|
|
/// <summary>Checks if TTS is available on this system.</summary>
|
|
/// <returns>True if TTS is available on this system.</returns>
|
|
public bool isTTSAvailable
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.Voices.Count > 0;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Checks if RT-Voice is speaking on this system.</summary>
|
|
/// <returns>True if RT-Voice is speaking on this system.</returns>
|
|
public bool isSpeaking => SpeechCount > 0;
|
|
|
|
/// <summary>Checks if RT-Voice is busy on this system.</summary>
|
|
/// <returns>True if RT-Voice is busy on this system.</returns>
|
|
public bool isBusy => BusyCount > 0;
|
|
|
|
/// <summary>Is standalone TTS enforced?</summary>
|
|
public bool enforcedStandaloneTTS { get; private set; }
|
|
|
|
/// <summary>Is RT-Voice paused?</summary>
|
|
public bool isPaused { get; private set; }
|
|
|
|
/// <summary>Is RT-Voice muted?</summary>
|
|
public bool isMuted { get; private set; }
|
|
|
|
|
|
#region Provider delegates
|
|
|
|
/// <summary>Returns the extension of the generated audio files.</summary>
|
|
/// <returns>Extension of the generated audio files.</returns>
|
|
public string AudioFileExtension
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.AudioFileExtension;
|
|
|
|
logVPIsNull();
|
|
|
|
return ".wav"; //best guess
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the default voice name of the current TTS-provider.</summary>
|
|
/// <returns>Default voice name of the current TTS-provider.</returns>
|
|
public string DefaultVoiceName
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.DefaultVoiceName;
|
|
|
|
logVPIsNull();
|
|
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>Get all available voices from the current TTS-system.</summary>
|
|
/// <returns>All available voices (alphabetically ordered by 'Name') as a list.</returns>
|
|
public System.Collections.Generic.List<Model.Voice> Voices
|
|
{
|
|
get
|
|
{
|
|
//Debug.Log($"Voices: {voiceProvider}");
|
|
if (voiceProvider != null)
|
|
return voiceProvider.Voices;
|
|
|
|
logVPIsNull();
|
|
|
|
return new System.Collections.Generic.List<Model.Voice>();
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is working directly inside the Unity Editor (without 'Play'-mode).</summary>
|
|
/// <returns>True if this TTS-system is working directly inside the Unity Editor.</returns>
|
|
public bool isWorkingInEditor
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isWorkingInEditor;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is working with 'Play'-mode inside the Unity Editor.</summary>
|
|
/// <returns>True if this TTS-system is working with 'Play'-mode inside the Unity Editor.</returns>
|
|
public bool isWorkingInPlaymode
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isWorkingInPlaymode;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Maximal length of the speech text (in characters) for the current TTS-system.</summary>
|
|
/// <returns>The maximal length of the speech text.</returns>
|
|
public int MaxTextLength
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.MaxTextLength;
|
|
|
|
logVPIsNull();
|
|
|
|
return 3999; //minimum (Android)
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is supporting SpeakNative.</summary>
|
|
/// <returns>True if this TTS-system supports SpeakNative.</returns>
|
|
public bool isSpeakNativeSupported
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isSpeakNativeSupported;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is supporting Speak.</summary>
|
|
/// <returns>True if this TTS-system supports Speak.</returns>
|
|
public bool isSpeakSupported
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isSpeakSupported;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is supporting the current platform.</summary>
|
|
/// <returns>True if this TTS-system supports current platform.</returns>
|
|
public bool isPlatformSupported => voiceProvider?.isPlatformSupported == true;
|
|
|
|
/// <summary>Indicates if this TTS-system is supporting SSML.</summary>
|
|
/// <returns>True if this TTS-system supports SSML.</returns>
|
|
public bool isSSMLSupported
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isSSMLSupported;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is an online service like MaryTTS or AWS Polly.</summary>
|
|
/// <returns>True if this TTS-system is an online service.</returns>
|
|
public bool isOnlineService
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isOnlineService;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system uses co-routines.</summary>
|
|
/// <returns>True if this TTS-system uses co-routines.</returns>
|
|
public bool hasCoRoutines
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.hasCoRoutines;
|
|
|
|
logVPIsNull();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this TTS-system is supporting IL2CPP.</summary>
|
|
/// <returns>True if this TTS-system supports IL2CPP.</returns>
|
|
public bool isIL2CPPSupported
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.isIL2CPPSupported;
|
|
|
|
logVPIsNull();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>Indicates if this provider returns voices in the Editor mode.</summary>
|
|
/// <returns>True if this provider returns voices in the Editor mode.</returns>
|
|
public bool hasVoicesInEditor
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.hasVoicesInEditor;
|
|
|
|
logVPIsNull();
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Get all available cultures from the current TTS-system (ISO 639-1).</summary>
|
|
/// <returns>All available cultures (alphabetically ordered by 'Culture') as a list.</returns>
|
|
public System.Collections.Generic.List<string> Cultures
|
|
{
|
|
get
|
|
{
|
|
if (voiceProvider != null)
|
|
return voiceProvider.Cultures;
|
|
|
|
logVPIsNull();
|
|
|
|
return new System.Collections.Generic.List<string>();
|
|
}
|
|
}
|
|
|
|
/// <summary>Get all available speech engines (works only for Android).</summary>
|
|
/// <returns>All available speech engines as a list.</returns>
|
|
public System.Collections.Generic.List<string> 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<string>();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#endregion
|
|
|
|
|
|
#region Events
|
|
|
|
[Header("Events")] public VoicesReadyEvent OnReady;
|
|
public SpeakStartEvent OnSpeakStarted;
|
|
public SpeakCompleteEvent OnSpeakCompleted;
|
|
public ProviderChangeEvent OnProviderChanged;
|
|
public ErrorEvent OnError;
|
|
|
|
|
|
/// <summary>An event triggered whenever the voices of a provider are ready.</summary>
|
|
public event VoicesReady OnVoicesReady;
|
|
|
|
/// <summary>An event triggered whenever a speak is started.</summary>
|
|
public event SpeakStart OnSpeakStart;
|
|
|
|
/// <summary>An event triggered whenever a speak is completed.</summary>
|
|
public event SpeakComplete OnSpeakComplete;
|
|
|
|
/// <summary>An event triggered whenever a new word is spoken (native, Windows and iOS only).</summary>
|
|
public event SpeakCurrentWord OnSpeakCurrentWord;
|
|
|
|
/// <summary>An event triggered whenever a new phoneme is spoken (native, Windows only).</summary>
|
|
public event SpeakCurrentPhoneme OnSpeakCurrentPhoneme;
|
|
|
|
/// <summary>An event triggered whenever a new viseme is spoken (native, Windows only).</summary>
|
|
public event SpeakCurrentViseme OnSpeakCurrentViseme;
|
|
|
|
/// <summary>An event triggered whenever a speak audio generation is started.</summary>
|
|
public event SpeakAudioGenerationStart OnSpeakAudioGenerationStart;
|
|
|
|
/// <summary>An event triggered whenever a speak audio generation is completed.</summary>
|
|
public event SpeakAudioGenerationComplete OnSpeakAudioGenerationComplete;
|
|
|
|
/// <summary>An event triggered whenever a provider changes (e.g. Windows to MaryTTS).</summary>
|
|
public event ProviderChange OnProviderChange;
|
|
|
|
/// <summary>An event triggered whenever an error occurs.</summary>
|
|
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<string, AudioSource> 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<string, AudioSource> source in removeSources)
|
|
{
|
|
genericSources.Remove(source.Key);
|
|
Destroy(source.Value);
|
|
}
|
|
|
|
removeSources.Clear();
|
|
}
|
|
|
|
if (providedSources.Count > 0)
|
|
{
|
|
foreach (System.Collections.Generic.KeyValuePair<string, AudioSource> 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<string, AudioSource> 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
|
|
|
|
/// <summary>Resets this object.</summary>
|
|
//[RuntimeInitializeOnLoadMethod]
|
|
public static void ResetObject()
|
|
{
|
|
DeleteInstance();
|
|
loggedVPIsNull = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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"!
|
|
/// </summary>
|
|
/// <param name="text">Text for the length approximation.</param>
|
|
/// <param name="rate">Speech rate of the speaker in percent for the length approximation (1 = 100%, default: 1, optional).</param>
|
|
/// <param name="wordsPerMinute">Words per minute (default: 175, optional).</param>
|
|
/// <param name="timeFactor">Time factor for the calculated value (default: 0.9, optional).</param>
|
|
/// <returns>Approximated speech length in seconds of the given text and rate.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Is a voice available for a given gender and optional culture from the current TTS-system?</summary>
|
|
/// <param name="gender">Gender of the voice</param>
|
|
/// <param name="culture">Culture of the voice (e.g. "en", optional)</param>
|
|
/// <returns>True if a voice is available for a given gender and culture.</returns>
|
|
public bool isVoiceForGenderAvailable(Model.Enum.Gender gender, string culture = "")
|
|
{
|
|
return VoicesForGender(gender, culture).Count > 0;
|
|
}
|
|
|
|
/// <summary>Get all available voices for a given gender and optional culture from the current TTS-system.</summary>
|
|
/// <param name="gender">Gender of the voice</param>
|
|
/// <param name="culture">Culture of the voice (e.g. "en", optional)</param>
|
|
/// <param name="isFuzzy">Always returns voices if there is no match with the gender and/or culture (default: false, optional)</param>
|
|
/// <returns>All available voices (alphabetically ordered by 'Name') for a given gender and culture as a list.</returns>
|
|
public System.Collections.Generic.List<Model.Voice> VoicesForGender(Model.Enum.Gender gender, string culture = "", bool isFuzzy = false)
|
|
{
|
|
System.Collections.Generic.List<Model.Voice> voices = new System.Collections.Generic.List<Model.Voice>(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;
|
|
}
|
|
|
|
/// <summary>Get a voice from for a given gender and optional culture and optional index from the current TTS-system.</summary>
|
|
/// <param name="gender">Gender of the voice</param>
|
|
/// <param name="culture">Culture of the voice (e.g. "en", optional)</param>
|
|
/// <param name="index">Index of the voice (default: 0, optional)</param>
|
|
/// <param name="fallbackCulture">Fallback culture of the voice (e.g. "en", default "", optional)</param>
|
|
/// <param name="isFuzzy">Always returns voices if there is no match with the gender and/or culture (default: false, optional)</param>
|
|
/// <returns>Voice for the given culture and index.</returns>
|
|
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<Model.Voice> 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;
|
|
}
|
|
|
|
/// <summary>Is a voice available for a given culture from the current TTS-system?</summary>
|
|
/// <param name="culture">Culture of the voice (e.g. "en")</param>
|
|
/// <returns>True if a voice is available for a given culture.</returns>
|
|
public bool isVoiceForCultureAvailable(string culture)
|
|
{
|
|
return VoicesForCulture(culture).Count > 0;
|
|
}
|
|
|
|
/// <summary>Get all available voices for a given culture from the current TTS-system.</summary>
|
|
/// <param name="culture">Culture of the voice (e.g. "en")</param>
|
|
/// <param name="isFuzzy">Always returns voices if there is no match with the culture (default: false, optional)</param>
|
|
/// <returns>All available voices (alphabetically ordered by 'Name') for a given culture as a list.</returns>
|
|
public System.Collections.Generic.List<Model.Voice> 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<Model.Voice> voices = Voices.Where(s => s.SimplifiedCulture.StartsWith(_culture, System.StringComparison.OrdinalIgnoreCase)).OrderBy(s => s.Name).ToList();
|
|
#else
|
|
System.Collections.Generic.List<Model.Voice> 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;
|
|
}
|
|
|
|
/// <summary>Get a voice from for a given culture and optional index from the current TTS-system.</summary>
|
|
/// <param name="culture">Culture of the voice (e.g. "en")</param>
|
|
/// <param name="index">Index of the voice (default: 0, optional)</param>
|
|
/// <param name="fallbackCulture">Fallback culture of the voice (e.g. "en", default "", optional)</param>
|
|
/// <param name="isFuzzy">Always returns voices if there is no match with the culture (default: false, optional)</param>
|
|
/// <returns>Voice for the given culture and index.</returns>
|
|
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<Model.Voice> 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;
|
|
}
|
|
|
|
/// <summary>Is a voice available for a given name from the current TTS-system?</summary>
|
|
/// <param name="_name">Name of the voice (e.g. "Alex")</param>
|
|
/// <param name="isExact">Exact match for the voice name (default: false, optional)</param>
|
|
/// <returns>True if a voice is available for a given name.</returns>
|
|
public bool isVoiceForNameAvailable(string _name, bool isExact = false)
|
|
{
|
|
return VoiceForName(_name, isExact) != null;
|
|
}
|
|
|
|
/// <summary>Get a voice for a given name from the current TTS-system.</summary>
|
|
/// <param name="_name">Name of the voice (e.g. "Alex")</param>
|
|
/// <param name="isExact">Exact match for the voice name (default: false, optional)</param>
|
|
/// <returns>Voice for the given name or null if not found.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Speaks a text with a given voice (native mode).</summary>
|
|
/// <param name="text">Text to speak.</param>
|
|
/// <param name="voice">Voice to speak (optional).</param>
|
|
/// <param name="rate">Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional).</param>
|
|
/// <param name="pitch">Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional).</param>
|
|
/// <param name="volume">Volume of the speaker in percent (1 = 100%, values: 0-1, default: 1, optional).</param>
|
|
/// <param name="forceSSML">Force SSML on supported platforms (default: true, optional).</param>
|
|
/// <returns>UID of the speaker.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Speaks a text with a given voice (native mode).</summary>
|
|
/// <param name="wrapper">Speak wrapper.</param>
|
|
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<AudioSource>();
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Speaks a text with a given wrapper (native mode).</summary>
|
|
/// <param name="wrapper">Speak wrapper.</param>
|
|
/// <returns>UID of the speaker.</returns>
|
|
public string SpeakNative(Model.Wrapper wrapper)
|
|
{
|
|
if (wrapper != null)
|
|
{
|
|
SpeakNativeWithUID(wrapper);
|
|
|
|
return wrapper.Uid;
|
|
}
|
|
|
|
logWrapperIsNull();
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
/// <summary>Speaks a text with a given voice.</summary>
|
|
/// <param name="text">Text to speak.</param>
|
|
/// <param name="source">AudioSource for the output (optional).</param>
|
|
/// <param name="voice">Voice to speak (optional).</param>
|
|
/// <param name="speakImmediately">Speak the text immediately (default: true). Only works if 'Source' is not null.</param>
|
|
/// <param name="rate">Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional).</param>
|
|
/// <param name="pitch">Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional).</param>
|
|
/// <param name="volume">Volume of the speaker in percent (1 = 100%, values: 0-1, default: 1, optional).</param>
|
|
/// <param name="outputFile">Saves the generated audio to an output file (without extension, optional).</param>
|
|
/// <param name="forceSSML">Force SSML on supported platforms (default: true, optional).</param>
|
|
/// <returns>UID of the speaker.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Speaks a text with a given voice.</summary>
|
|
/// <param name="wrapper">Speak wrapper.</param>
|
|
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<AudioSource>();
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Speaks a text with a given wrapper.</summary>
|
|
/// <param name="wrapper">Speak wrapper.</param>
|
|
/// <returns>UID of the speaker.</returns>
|
|
public string Speak(Model.Wrapper wrapper)
|
|
{
|
|
if (wrapper != null)
|
|
{
|
|
SpeakWithUID(wrapper);
|
|
|
|
return wrapper.Uid;
|
|
}
|
|
|
|
logWrapperIsNull();
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
/// <summary>Speaks and marks a text with a given wrapper.</summary>
|
|
/// <param name="wrapper">Speak wrapper.</param>
|
|
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();
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>Speaks and marks a text with a given voice and tracks the word position.</summary>
|
|
/// <param name="uid">UID of the speaker</param>
|
|
/// <param name="text">Text to speak.</param>
|
|
/// <param name="source">AudioSource for the output.</param>
|
|
/// <param name="voice">Voice to speak (optional).</param>
|
|
/// <param name="rate">Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional).</param>
|
|
/// <param name="pitch">Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional).</param>
|
|
/// <param name="forceSSML">Force SSML on supported platforms (default: true, optional).</param>
|
|
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));
|
|
}
|
|
|
|
// /// <summary>
|
|
// /// Speaks a text with a given voice and tracks the word position.
|
|
// /// </summary>
|
|
// 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;
|
|
// }
|
|
|
|
/// <summary>Generates an audio file from a given wrapper.</summary>
|
|
/// <param name="wrapper">Speak wrapper.</param>
|
|
/// <returns>UID of the generator.</returns>
|
|
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;
|
|
}
|
|
|
|
|
|
/// <summary>Generates an audio file from a text with a given voice.</summary>
|
|
/// <param name="text">Text to generate.</param>
|
|
/// <param name="outputFile">Saves the generated audio to an output file (without extension).</param>
|
|
/// <param name="voice">Voice to speak (optional).</param>
|
|
/// <param name="rate">Speech rate of the speaker in percent (1 = 100%, values: 0-3, default: 1, optional).</param>
|
|
/// <param name="pitch">Pitch of the speech in percent (1 = 100%, values: 0-2, default: 1, optional).</param>
|
|
/// <param name="volume">Volume of the speaker in percent (1 = 100%, values: 0-1, default: 1, optional).</param>
|
|
/// <param name="forceSSML">Force SSML on supported platforms (default: true, optional).</param>
|
|
/// <returns>UID of the generator.</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Silence all active TTS-voices.</summary>
|
|
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<string, AudioSource> source in genericSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.Stop();
|
|
Destroy(source.Value, 0.1f);
|
|
}
|
|
|
|
genericSources.Clear();
|
|
|
|
foreach (System.Collections.Generic.KeyValuePair<string, AudioSource> source in providedSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
providedSources.Clear();
|
|
|
|
if (!Common.Util.BaseHelper.isEditorMode)
|
|
logVPIsNull();
|
|
}
|
|
|
|
SpeechCount = 0;
|
|
BusyCount = 0;
|
|
}
|
|
|
|
/// <summary>Silence all active TTS-voices (optional with a UID).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
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--;
|
|
}
|
|
|
|
/// <summary>Pause all active TTS-voices (optional with a UID, only for 'Speak'-calls).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
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<string, AudioSource> source in genericSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.Pause();
|
|
}
|
|
|
|
foreach (System.Collections.Generic.KeyValuePair<string, AudioSource> source in providedSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.Pause();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logVPIsNull();
|
|
}
|
|
#if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>Un-Pause all active TTS-voices (optional with a UID, only for 'Speak'-calls).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
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<string, AudioSource> source in genericSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.UnPause();
|
|
}
|
|
|
|
foreach (System.Collections.Generic.KeyValuePair<string, AudioSource> source in providedSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.UnPause();
|
|
}
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
logVPIsNull();
|
|
}
|
|
#if (UNITY_IOS || UNITY_TVOS) && !UNITY_EDITOR
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>Pause or unpause all active TTS-voices (optional with a UID, only for 'Speak'-calls).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
public void PauseOrUnPause(string uid = null)
|
|
{
|
|
if (isPaused)
|
|
{
|
|
UnPause(uid);
|
|
}
|
|
else
|
|
{
|
|
Pause(uid);
|
|
}
|
|
}
|
|
|
|
/// <summary>Mute all active TTS-voices (optional with a UID, only for 'Speak'-calls).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
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<string, AudioSource> source in genericSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.mute = true;
|
|
}
|
|
|
|
foreach (System.Collections.Generic.KeyValuePair<string, AudioSource> source in providedSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.mute = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logVPIsNull();
|
|
}
|
|
}
|
|
|
|
/// <summary>Un-mute all active TTS-voices (optional with a UID, only for 'Speak'-calls).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
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<string, AudioSource> source in genericSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.mute = false;
|
|
}
|
|
|
|
foreach (System.Collections.Generic.KeyValuePair<string, AudioSource> source in providedSources.Where(source => source.Value != null))
|
|
{
|
|
source.Value.mute = false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logVPIsNull();
|
|
}
|
|
}
|
|
|
|
/// <summary>Mute or unmute all active TTS-voices (optional with a UID, only for 'Speak'-calls).</summary>
|
|
/// <param name="uid">UID of the speaker (optional)</param>
|
|
public void MuteOrUnMute(string uid = null)
|
|
{
|
|
if (isMuted)
|
|
{
|
|
UnMute(uid);
|
|
}
|
|
else
|
|
{
|
|
Mute(uid);
|
|
}
|
|
}
|
|
|
|
/// <summary>Reloads the provider.</summary>
|
|
public void ReloadProvider()
|
|
{
|
|
Silence();
|
|
initProvider();
|
|
}
|
|
|
|
/// <summary>Deletes all generated audio files.</summary>
|
|
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
|
|
/// <summary>Sets all voices from iOS.</summary>
|
|
/// <param name="voices">All voices from iOS.</param>
|
|
public void SetVoices(string voices)
|
|
{
|
|
Provider.VoiceProviderIOS.SetVoices(voices);
|
|
}
|
|
|
|
/// <summary>The current spoken word from iOS.</summary>
|
|
/// <param name="voices">Current spoken word from iOS.</param>
|
|
public void WordSpoken(string word)
|
|
{
|
|
Provider.VoiceProviderIOS.WordSpoken();
|
|
}
|
|
|
|
/// <summary>Sets the state from iOS.</summary>
|
|
/// <param name="voices">State from iOS.</param>
|
|
public void SetState(string state)
|
|
{
|
|
Provider.VoiceProviderIOS.SetState(state);
|
|
}
|
|
#endif
|
|
|
|
#endregion
|
|
}
|
|
}
|
|
// © 2015-2020 crosstales LLC (https://www.crosstales.com) |