软件中的语音技术主要包含两种:语音识别speech recognition和语音合成speech synthesis。通常地,开发者会由于技术实力和资金实力等各方面的问题无力完成专业的语音引擎,所以一般选择现有的较为专业的语音引擎来完成相关的开发,好比国内很是出名的科大讯飞,百度语音等等。固然国外的还有Google语音,微软有SAPI等等。git
在VR开发过程当中,因为运行在Windows环境下,那么天然而然,咱们首选SAPI来进行语音开发。一是和Windows原生,二是离线不须要网络,三是不须要任何插件。另外就是SAPI发音,尤为是英文发音,仍是相对来讲质量不错的。(Win7以上自带)github
使用SAPI,须要使用到的是System.Speech.dll文件。因为Unity须要将Dll文件放在Asset目录下,而这样的结果会发现sapi failed to initialize。缘由怀疑为须要特定的上下文环境才能运行dll的api,以致于拷贝到Asset目录致使上下文环境缺失而没法运行。api
可是若是作过这方面开发的知道,在C#的其余应用里面引用System.Speech.dll是彻底没有问题的。那么是否是咱们能够开发一个专门的第三方程序,而后unity进行调用呢?按照这个思路,咱们开发了一个控制台程序Speech.exe,主要功能是根据输入文本进行语音合成。服务器
代码较为简单网络
/*简单的SAPI语音合成控制台程序*/socket
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参数。this
/*从参数读取须要发音的文本*/spa
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