#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX //|| CT_DEVELOP using UnityEngine; using System.Collections; using System.Linq; namespace Crosstales.RTVoice.Provider { /// MacOS voice provider. public class VoiceProviderMacOS : BaseVoiceProvider { #region Variables private static readonly System.Text.RegularExpressions.Regex sayRegex = new System.Text.RegularExpressions.Regex(@"^([^#]+?)\s*([^ ]+)\s*# (.*?)$"); private const int defaultRate = 175; #if ENABLE_IL2CPP private System.Collections.Generic.Dictionary processCreators = new System.Collections.Generic.Dictionary(); #endif private bool isLoading; #endregion #region Properties /* /// Returns the singleton instance of this class. /// Singleton instance of this class. public static VoiceProviderMacOS Instance => instance ?? (instance = new VoiceProviderMacOS()); */ public override string AudioFileExtension => ".wav"; public override AudioType AudioFileType => AudioType.WAV; public override string DefaultVoiceName => "Alex"; public override bool isWorkingInEditor => Util.Helper.isMacOSEditor; public override bool isWorkingInPlaymode => Util.Helper.isMacOSEditor; public override int MaxTextLength => 256000; public override bool isSpeakNativeSupported => true; public override bool isSpeakSupported => true; public override bool isPlatformSupported => Util.Helper.isMacOSPlatform; public override bool isSSMLSupported => false; public override bool isOnlineService => false; public override bool hasCoRoutines => true; public override bool isIL2CPPSupported => true; public override bool hasVoicesInEditor => true; #endregion #region Implemented methods public override void Load(bool forceReload = false) { if (cachedVoices?.Count == 0 || forceReload) { if (Util.Helper.isEditorMode) { #if UNITY_EDITOR getVoicesInEditor(); #endif } else { if (!isLoading) { isLoading = true; Speaker.Instance.StartCoroutine(getVoices()); } } } else { onVoicesReady(); } } public override IEnumerator SpeakNative(Model.Wrapper wrapper) { if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { yield return null; //return to the main process (uid) string voiceName = getVoiceName(wrapper); int calculatedRate = calculateRate(wrapper.Rate); #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { string args = (string.IsNullOrEmpty(voiceName) ? string.Empty : " -v \"" + voiceName.Replace('"', '\'') + '"') + (calculatedRate != defaultRate ? " -r " + calculatedRate : string.Empty) + " \"" + wrapper.Text.Replace('"', '\'') + '"'; if (Util.Config.DEBUG) Debug.Log("Process arguments: " + args); process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = args; System.Threading.Thread worker = new System.Threading.Thread(() => startProcess(process, 0, false, false, false)) {Name = wrapper.Uid}; worker.Start(); silence = false; #if ENABLE_IL2CPP processCreators.Add(wrapper.Uid, process); #else processes.Add(wrapper.Uid, process); #endif onSpeakStart(wrapper); do { yield return null; } while (worker.IsAlive || !process.HasExited); #if ENABLE_IL2CPP if (process.ExitCode == 0 || process.ExitCode == 123456) //123456 = Killed #else if (process.ExitCode == 0 || process.ExitCode == -1 || process.ExitCode == 137) //0 = normal ended, -1/137 = killed #endif { if (Util.Config.DEBUG) Debug.Log("Text spoken: " + wrapper.Text); onSpeakComplete(wrapper); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not speak the text: " + wrapper + System.Environment.NewLine + "Exit code: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); onErrorInfo(wrapper, errorMessage); } } #if ENABLE_IL2CPP processCreators.Remove(wrapper.Uid); #else processes.Remove(wrapper.Uid); #endif } } } } public override IEnumerator Speak(Model.Wrapper wrapper) { if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { if (wrapper.Source == null) { Debug.LogWarning("'wrapper.Source' is null: " + wrapper); } else { yield return null; //return to the main process (uid) string voiceName = getVoiceName(wrapper); int calculatedRate = calculateRate(wrapper.Rate); string outputFile = getOutputFile(wrapper.Uid); #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { string args = (string.IsNullOrEmpty(voiceName) ? string.Empty : $" -v \"{voiceName.Replace('"', '\'')}\"") + (calculatedRate != defaultRate ? $" -r {calculatedRate}" : string.Empty) + $" -o \"{outputFile.Replace('"', '\'')}\"" + //" --file-format=AIFFLE" " --file-format=WAVE" + " --data-format=LEI16@22050" + $" \"{wrapper.Text.Replace('"', '\'')}\""; if (Util.Config.DEBUG) Debug.Log("Process arguments: " + args); process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = args; System.Threading.Thread worker = new System.Threading.Thread(() => startProcess(process, 0, false, false, false)) {Name = wrapper.Uid}; worker.Start(); silence = false; #if ENABLE_IL2CPP processCreators.Add(wrapper.Uid, process); #else processes.Add(wrapper.Uid, process); #endif onSpeakAudioGenerationStart(wrapper); do { yield return null; } while (worker.IsAlive || !process.HasExited); if (process.ExitCode == 0) { yield return playAudioFile(wrapper, Util.Constants.PREFIX_FILE + outputFile, outputFile, AudioFileType); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not speak the text: " + wrapper + System.Environment.NewLine + "Exit code: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); onErrorInfo(wrapper, errorMessage); } } #if ENABLE_IL2CPP processCreators.Remove(wrapper.Uid); #else processes.Remove(wrapper.Uid); #endif } } } } } public override IEnumerator Generate(Model.Wrapper wrapper) { if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { yield return null; //return to the main process (uid) string voiceName = getVoiceName(wrapper); int calculatedRate = calculateRate(wrapper.Rate); string outputFile = getOutputFile(wrapper.Uid); #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { string args = (string.IsNullOrEmpty(voiceName) ? string.Empty : $" -v \"{voiceName.Replace('"', '\'')}\"") + (calculatedRate != defaultRate ? $" -r {calculatedRate}" : string.Empty) + $" -o \"{outputFile.Replace('"', '\'')}\"" + //" --file-format=AIFFLE" " --file-format=WAVE" + " --data-format=LEI16@22050" + $" \"{wrapper.Text.Replace('"', '\'')}\""; if (Util.Config.DEBUG) Debug.Log("Process arguments: " + args); process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = args; System.Threading.Thread worker = new System.Threading.Thread(() => startProcess(process, 0, false, false, false)) {Name = wrapper.Uid}; worker.Start(); silence = false; #if ENABLE_IL2CPP processCreators.Add(wrapper.Uid, process); #else processes.Add(wrapper.Uid, process); #endif onSpeakAudioGenerationStart(wrapper); do { yield return null; } while (worker.IsAlive || !process.HasExited); if (process.ExitCode == 0) { processAudioFile(wrapper, outputFile); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not generate the text: " + wrapper + System.Environment.NewLine + "Exit code: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); onErrorInfo(wrapper, errorMessage); } } #if ENABLE_IL2CPP processCreators.Remove(wrapper.Uid); #else processes.Remove(wrapper.Uid); #endif } } } } public override void Silence() { base.Silence(); #if ENABLE_IL2CPP foreach (var kvp in processCreators.Where(kvp => kvp.Value.isBusy)) { kvp.Value.Kill(); } processCreators.Clear(); #endif } public override void Silence(string uid) { base.Silence(uid); #if ENABLE_IL2CPP if (!string.IsNullOrEmpty(uid)) { if (processCreators.ContainsKey(uid)) { if (processCreators[uid].isBusy) processCreators[uid].Kill(); processCreators.Remove(uid); } } #endif } #endregion #region Private methods private IEnumerator getVoices() { #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = "-v ?"; process.Start(); System.Threading.Thread worker = new System.Threading.Thread(() => startProcess(process, Util.Constants.DEFAULT_TTS_KILL_TIME)); worker.Start(); do { yield return null; } while (worker.IsAlive || !process.HasExited); if (process.ExitCode == 0) { System.Collections.Generic.List voices = new System.Collections.Generic.List(60); using (System.IO.StreamReader streamReader = process.StandardOutput) { while (!streamReader.EndOfStream) { string reply = streamReader.ReadLine(); if (!string.IsNullOrEmpty(reply)) { System.Text.RegularExpressions.Match match = sayRegex.Match(reply); if (match.Success) { string name = match.Groups[1].ToString(); voices.Add(new Model.Voice(name, match.Groups[3].ToString(), Util.Helper.AppleVoiceNameToGender(name), "unknown", match.Groups[2].ToString().Replace('_', '-'), string.Empty, "Apple")); } } } } cachedVoices = voices.OrderBy(s => s.Name).ToList(); if (Util.Constants.DEV_DEBUG) Debug.Log("Voices read: " + cachedVoices.CTDump()); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not get any voices: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); onErrorInfo(null, errorMessage); } } } isLoading = false; onVoicesReady(); } private static int calculateRate(float rate) { int result = Mathf.Clamp( Mathf.Abs(rate - 1f) > Common.Util.BaseConstants.FLOAT_TOLERANCE ? (int)(defaultRate * rate) : defaultRate, 1, 3 * defaultRate); if (Util.Constants.DEV_DEBUG) Debug.Log("calculateRate: " + result + " - " + rate); return result; } #endregion #region Editor-only methods #if UNITY_EDITOR public override void GenerateInEditor(Model.Wrapper wrapper) { if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { string voiceName = getVoiceName(wrapper); int calculatedRate = calculateRate(wrapper.Rate); string outputFile = getOutputFile(wrapper.Uid); #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { string args = (string.IsNullOrEmpty(voiceName) ? string.Empty : $" -v \"{voiceName.Replace('"', '\'')}\"") + (calculatedRate != defaultRate ? $" -r {calculatedRate}" : string.Empty) + $" -o \"{outputFile.Replace('"', '\'')}\"" + //" --file-format=AIFFLE" " --file-format=WAVE" + " --data-format=LEI16@22050" + $" \"{wrapper.Text.Replace('"', '\'')}\""; if (Util.Config.DEBUG) Debug.Log("Process arguments: " + args); process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = args; System.Threading.Thread worker = new System.Threading.Thread(() => startProcess(process, 0, false, false, false)) {Name = wrapper.Uid}; worker.Start(); silence = false; onSpeakAudioGenerationStart(wrapper); do { System.Threading.Thread.Sleep(50); } while (worker.IsAlive || !process.HasExited); if (process.ExitCode == 0) { processAudioFile(wrapper, outputFile); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not generate the text: " + wrapper + System.Environment.NewLine + "Exit code: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); onErrorInfo(wrapper, errorMessage); } } } } } } public override void SpeakNativeInEditor(Model.Wrapper wrapper) { if (wrapper == null) { Debug.LogWarning("'wrapper' is null!"); } else { if (string.IsNullOrEmpty(wrapper.Text)) { Debug.LogWarning("'wrapper.Text' is null or empty: " + wrapper); } else { string voiceName = getVoiceName(wrapper); int calculatedRate = calculateRate(wrapper.Rate); #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { string args = (string.IsNullOrEmpty(voiceName) ? string.Empty : " -v \"" + voiceName.Replace('"', '\'') + '"') + (calculatedRate != defaultRate ? " -r " + calculatedRate : string.Empty) + " \"" + wrapper.Text.Replace('"', '\'') + '"'; if (Util.Config.DEBUG) Debug.Log("Process arguments: " + args); process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = args; System.Threading.Thread worker = new System.Threading.Thread(() => startProcess(process, 0, false, false, false)) {Name = wrapper.Uid}; worker.Start(); silence = false; onSpeakStart(wrapper); do { System.Threading.Thread.Sleep(50); if (silence && !process.HasExited) { process.Kill(); } } while (worker.IsAlive || !process.HasExited); #if ENABLE_IL2CPP if (process.ExitCode == 0 || process.ExitCode == 123456) //123456 = Killed #else if (process.ExitCode == 0 || process.ExitCode == -1 || process.ExitCode == 137 ) //0 = normal ended, -1/137 = killed #endif { if (Util.Config.DEBUG) Debug.Log("Text spoken: " + wrapper.Text); onSpeakComplete(wrapper); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not speak the text: " + wrapper + System.Environment.NewLine + "Exit code: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); onErrorInfo(wrapper, errorMessage); } } } } } } private void getVoicesInEditor() { #if ENABLE_IL2CPP using (Common.Util.CTProcess process = new Common.Util.CTProcess()) #else using (System.Diagnostics.Process process = new System.Diagnostics.Process()) #endif { process.StartInfo.FileName = Util.Config.TTS_MACOS; process.StartInfo.Arguments = "-v ?"; try { System.Threading.Thread voiceWorker = new System.Threading.Thread(() => startProcess(process, Util.Constants.DEFAULT_TTS_KILL_TIME)); voiceWorker.Start(); do { System.Threading.Thread.Sleep(50); } while (voiceWorker.IsAlive || !process.HasExited); if (Util.Constants.DEV_DEBUG) Debug.Log("Finished after: " + (process.ExitTime - process.StartTime).Seconds); if (process.ExitCode == 0) { System.Collections.Generic.List voices = new System.Collections.Generic.List(100); using (System.IO.StreamReader streamReader = process.StandardOutput) { while (!streamReader.EndOfStream) { string reply = streamReader.ReadLine(); if (!string.IsNullOrEmpty(reply)) { System.Text.RegularExpressions.Match match = sayRegex.Match(reply); if (match.Success) { string name = match.Groups[1].ToString(); voices.Add(new Model.Voice(match.Groups[1].ToString(), match.Groups[3].ToString(), Util.Helper.AppleVoiceNameToGender(name), "unknown", match.Groups[2].ToString().Replace('_', '-'))); } } } } cachedVoices = voices.OrderBy(s => s.Name).ToList(); if (Util.Constants.DEV_DEBUG) Debug.Log("Voices read: " + cachedVoices.CTDump()); } else { using (System.IO.StreamReader sr = process.StandardError) { string errorMessage = "Could not get any voices: " + process.ExitCode + System.Environment.NewLine + sr.ReadToEnd(); Debug.LogError(errorMessage); } } } catch (System.Exception ex) { string errorMessage = "Could not get any voices!" + System.Environment.NewLine + ex; Debug.LogError(errorMessage); } } onVoicesReady(); } #endif #endregion } } #endif // © 2015-2020 crosstales LLC (https://www.crosstales.com)