軟件中的語音技術主要包含兩種:語音識別speech recognition和語音合成speech synthesis。一般地,開發者會因為技術實力和資金實力等各方面的問題無力完成專業的語音引擎,因此通常選擇現有的較為專業的語音引擎來完成相關的開發,比如國內非常出名的科大訊飛,百度語音等等。當然國外的還有Google語音,微軟有SAPI等等。
在VR開發過程中,由於運行在Windows環境下,那么自然而然,我們首選SAPI來進行語音開發。一是和Windows原生,二是離線不需要網絡,三是不需要任何插件。另外就是SAPI發音,尤其是英文發音,還是相對來說質量不錯的。(Win7以上自帶)
使用SAPI,需要使用到的是System.Speech.dll文件。由於Unity需要將Dll文件放在Asset目錄下,而這樣的結果會發現sapi failed to initialize。原因懷疑為需要特定的上下文環境才能運行dll的api,以至於拷貝到Asset目錄導致上下文環境缺失而無法運行。
但是如果做過這方面開發的知道,在C#的其他應用里面引用System.Speech.dll是完全沒有問題的。那么是不是我們可以開發一個專門的第三方程序,然后unity進行調用呢?按照這個思路,我們開發了一個控制台程序Speech.exe,主要功能是根據輸入文本進行語音合成。
代碼較為簡單
/*簡單的SAPI語音合成控制台程序*/
using System.Speech.Synthesis; using SpeechTest.Properties; namespace SpeechTest { class Program { static void Main(string[] args) { var speaker = new SpeechSynthesizer(); speaker.Speak(“test”); } } }
OK,運行就可以聽到機器發音Test了。
我們修改一下,改為從參數中讀取,這樣的話,我們可以在unity中利用Process運行Speech.exe,並傳給Speech參數。
/*從參數讀取需要發音的文本*/
using System.Speech.Synthesis; using SpeechTest.Properties; namespace SpeechTest { class Program { static void Main(string[] args) { var speaker = new SpeechSynthesizer(); var res = args.Length == 0 ? "請說" : args[0]; speaker.Speak(res); } } }
我們先使用CMD命令行,cd到Speech.exe所在的目錄,然后輸入Speech.exe test,如我們預想的那般,機器發音test。測試通過。
為了能夠更改發音的配置,增加一些代碼,從Setting中讀取相關的配置數據,代碼更改如下:
/*能夠配置的控制台程序*/
using System.Speech.Synthesis; using SpeechTest.Properties; namespace SpeechTest { class Program { static void Main(string[] args) { var speaker = new SpeechSynthesizer(); speaker.Volume = Settings.Default.SpeakVolume; speaker.Rate = Settings.Default.SpeakRate; var voice = Settings.Default.SpeakVoice; if (!string.IsNullOrEmpty(voice)) speaker.SelectVoice(voice); var res = args.Length == 0 ? "請說" : args[0]; speaker.Speak(res); } } }
接下來我們在Unity中使用Process來開啟這個Speech.exe,代碼如下:
/*Unity中開啟Speech.exe進程*/
using System.Diagnostics; public class Speecher: MonoBehaviour { public static void Speak(string str) { var proc = new Process { StartInfo = new ProcessStartInfo { FileName = "speech.exe", Arguments = "\"" + str + "\"", } }; proc.Start(); } /***測試代碼,可刪除Start***/ protected void Start() { Speak("test"); } /***測試代碼,可刪除End***/ }
將腳本掛在任何一個GO(GameObject)上,運行,黑框出現,同時聽到發音,測試完成。
接下來我們隱藏這個黑框。代碼修改如下:
/*Unity開啟無框的Speech.exe進程*/
using System.Diagnostics; public class Speecher: MonoBehaviour { public static void Speak(string str) { var proc = new Process { StartInfo = new ProcessStartInfo { FileName = "speech.exe", Arguments = "\"" + str + "\"", CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden, } }; proc.Start(); } /***測試代碼,可刪除Start***/ protected void Start() { Speak("test"); } /***測試代碼,可刪除End***/ }
其實到了這一步,主要的功能都完成了。但是細心的會發現,這樣不斷創建進程然后關閉進程的方式會不會太笨了。可不可以讓Speech這個進程一直開啟着,收到unity的信息時就發音呢?這就涉及到進程間通信了。
Windows的進程是相互獨立的,各自有各自的分配空間。但是並不意味這不能相互通信。方法有很多,比如讀寫文件,發送消息(hook),Socket等等。其中Socket實現起來相對簡單,尤其是我們已經擁有Socket封裝庫的情況下,只要少量代碼就行了。
於是在Speech改成一個Socket服務器,代碼如下:
/*Speech 服務端*/
using System; using System.Linq; using System.Speech.Synthesis; using System.Text; using Speech.Properties; namespace Speech { class Program { static void Main(string[] args) { var server = new NetServer(); server.StartServer(); while (true) { var res = Console.ReadLine(); if (res == "exit") break; } } } public class NetServer : SocketExtra.INetComponent { private readonly Speecher m_speecher; private readonly SocketExtra m_socket; public NetServer() { m_speecher = new Speecher(); m_socket = new SocketExtra(this); } public void StartServer() { m_socket.Bind("127.0.0.1", Settings.Default.Port); } public bool NetSendMsg(byte[] sendbuffer) { return true; } public bool NetReciveMsg(byte[] recivebuffer) { var str = Encoding.Default.GetString(recivebuffer); Console.WriteLine(str); m_speecher.Speak(str); return true; } public bool Connected { get { return m_socket.Connected; } } } public class Speecher { private readonly SpeechSynthesizer m_speaker; public Speecher() { m_speaker = new SpeechSynthesizer(); var installs = m_speaker.GetInstalledVoices(); m_speaker.Volume = Settings.Default.SpeakVolume; m_speaker.Rate = Settings.Default.SpeakRate; var voice = Settings.Default.SpeakVoice; var selected = false; if (!string.IsNullOrEmpty(voice)) { if (installs.Any(install => install.VoiceInfo.Name == voice)) { m_speaker.SelectVoice(voice); selected = true; } } if (!selected) { foreach (var install in installs.Where(install => install.VoiceInfo.Culture.Name == "en-US")) { m_speaker.SelectVoice(install.VoiceInfo.Name); break; } } } public void Speak(string msg) { m_speaker.Speak(msg); } } }
同時修改Unity代碼,增加Socket相關代碼:
/*Unity客戶端代碼*/
using System.Collections; using System.Diagnostics; using System.Text; using UnityEngine; public class Speecher : MonoBehaviour, SocketExtra.INetComponent { private SocketExtra m_socket; private Process m_process; protected void Awake() { Ins = this; m_process = new Process { StartInfo = new ProcessStartInfo { FileName = "speech.exe", CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden }, }; m_process.Start(); } /***測試代碼,可刪除Start***/ protected IEnumerator Start() { yield return StartCoroutine(Connect()); Speak("test"); } /***測試代碼,可刪除End***/ public IEnumerator Connect() { m_socket = new SocketExtra(this); m_socket.Connect("127.0.0.1", 9903); while (!m_socket.Connected) { yield return 1; } } protected void OnDestroy() { if (m_process != null && !m_process.HasExited) m_process.Kill(); m_process = null; } public static Speecher Ins; public static void Speak(string str) { #if UNITY_EDITOR||UNITY_STANDALONE_WIN Ins.Speech(str); #endif } public void Speech(string str) { if (m_socket.Connected) { var bytes = Encoding.Default.GetBytes(str); m_socket.SendMsg(bytes); } } public bool NetReciveMsg(byte[] recivebuffer) { return true; } public bool NetSendMsg(byte[] sendbuffer) { return true; } }
OK,大功告成。工程見Github
https://github.com/CodeGize/UnitySapi/
轉載請注明出處www.codegize.com