前言
讯飞语音是国内的智能语音前沿,有语音合成及语音识别还有一些其他高级的语音服务。前面已经写过一篇unity中使用在线语音的方式,不过由于是国外的网站不稳定,速度相对也慢,目前貌似已经不能用了。所以看好讯飞在线免费使用的优点,同时也看靠c++的而不是直接通过http请求的方式,在这里简要分享下在unity3d开发环境下,基于windows平台的在线语音生成。值得说明的讯飞的语音选择面广,还支持部分方言!但是对于创业型公司来说,离线版本的收费还是比较昂贵的,也是为什么本文只会涉及在线语音生成。
准备SDK
在讯飞语音官方网站(http://www.xfyun.cn/),可以下载到最新的SDK,不过要先注册,而注册后创建应用后生成的apiKey就是使用这些SDK的钥匙。解压后,应该能看到doc文件夹,如果你精通c语言,同时也熟练使用C#调用C的库,那么这篇文字对你意义不大,你需要的只是马上查看doc中的api去写在c#中实现c的接口了。当然因为本人对调用c的dll不是很熟练,所以才想把一些遇到的小问题记录下来,防止和我一样不熟悉的人会卡壳。在simple文件夹中有一些官方的demo,都是c写的,直接调用在bin中的msc.dll中的方法。在往下阅读之前,你或许可以先去学习下官方的例子。
提取接口
查看doc中的iFlytek MSC Reference Manual.html网页,能直接看到msc.dll中所有的api。如果你只关心语音生成,那么我们直接跳到qtts.h部分,这里面一共就只有5个接口,也就是说我们的工作量并不大,只需要搞清楚这几个接口就好了。
将这些接口转换到C#中,应该看起来是这样的:
[DllImport(mscdll, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr QTTSSessionBegin(string _params, ref int errorCode);
[DllImport(mscdll, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSTextPut(string sessionID, string textString, uint textLen, string _params);
[DllImport(mscdll, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern IntPtr QTTSAudioGet(string sessionID, ref int audioLen, ref SynthStatus synthStatus, ref int errorCode);
[DllImport(mscdll, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSSessionEnd(string sessionID, string hints);
[DllImport(mscdll, CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern int QTTSGetParam(string sessionID, string paramName, string paramValue, ref uint valueLen);
值得注意的是,因为涉及到中文章
QTTSTextPut
接口中CharSet必须要设置为
CharSet.Unicode
这是因为,如果在C#中调用dll,如果不指定的话,默认应该就传入Ascii码了,这样就会出现接收端为乱码的问题。
调用接口
在使用这几个接口的时候,可以参考官方demo中的tts.c文件,而这里实现的逻辑也和其中相差无几,唯一的区别就是,用的是C#无言也调用那几个接口。要说明的是本人很久没有用c++和c了,所以下面这段代码也不算原创,只是其中部分功能稍微调整了一下。
public void Speak(string speekText, string szParams, string outWaveFlie)
{
byte[] bytes = null;
int ret = 0;
try
{
sessionID = Marshal.PtrToStringAuto(MSPAPI.QTTSSessionBegin(szParams, ref ret));
if (ret != 0)
{
if (ttsSpeakErrorEvent != null) ttsSpeakErrorEvent.Invoke("初始化TTS引会话错误,错误代码:" + ret);
return;
}
ret = MSPAPI.QTTSTextPut(sessionID, speekText, (uint)Encoding.Unicode.GetByteCount(speekText), string.Empty);
if (ret != 0)
{
if (ttsSpeakErrorEvent != null) ttsSpeakErrorEvent.Invoke("向服务器发送数据,错误代码:" + ret);
return;
}
IntPtr audio_data;
int audio_len = 0;
SynthStatus synth_status = SynthStatus.MSP_TTS_FLAG_STILL_HAVE_DATA;
using (MemoryStream ms = new MemoryStream())
{
ms.Write(new byte[44], 0, 44);
//写44字节的空文件头
while (synth_status == SynthStatus.MSP_TTS_FLAG_STILL_HAVE_DATA)
{
audio_data = MSPAPI.QTTSAudioGet(sessionID, ref audio_len, ref synth_status, ref ret);
if (audio_data != IntPtr.Zero)
{
byte[] data = new byte[audio_len];
Marshal.Copy(audio_data, data, 0, audio_len);
ms.Write(data, 0, data.Length);
if (synth_status == SynthStatus.MSP_TTS_FLAG_DATA_END || ret != 0)
{
if (ret != 0)
{
if (ttsSpeakErrorEvent != null) ttsSpeakErrorEvent.Invoke("下载TTS文件错误,错误代码:" + ret);
return;
}
break;
}
}
Thread.Sleep(150);
}
System.Diagnostics.Debug.WriteLine("wav header");
WAVE_Header header = getWave_Header((int)ms.Length - 44); //创建wav文件头
byte[] headerByte = StructToBytes(header); //把文件头结构转化为字节数组 //写入文件头
ms.Position = 0; //定位到文件头
ms.Write(headerByte, 0, headerByte.Length); //写入文件头
bytes = ms.ToArray();
ms.Close();
}
if (outWaveFlie != null)
{
if (File.Exists(outWaveFlie))
{
File.Delete(outWaveFlie);
}
File.WriteAllBytes(outWaveFlie, bytes);
}
}
catch (Exception ex)
{
if (ttsSpeakErrorEvent != null) ttsSpeakErrorEvent.Invoke("Error:" + ex.Message);
return;
}
finally
{
ret = MSPAPI.QTTSSessionEnd(sessionID, "");
if (ret != 0)
{
if (ttsSpeakErrorEvent != null) ttsSpeakErrorEvent.Invoke("结束TTS会话错误,错误代码:" + ret);
}
else
{
if (tts_SpeakFinishedEvent != null) tts_SpeakFinishedEvent.Invoke(speekText, bytes);
}
}
}
其中要注意的一点是,因为c的dll中返回char*,在c#中,接收到的只能是指针,所以用了一个指针转换为字符串的方法:
Marshal.PtrToStringAuto
在对于字符串的长度问题,也没有直接使用string.Length,而是使用
(uint)Encoding.Unicode.GetByteCount(speekText)
封装为Unity模块
在上面已经实现了文字转语音功能的基础上,可以将这些功能封装为unity主线程中可以直接调用的一个模块,便于程序的使用。和前面一篇文章实现的接口是一样的,只是添加了一个批量下载的功能。当然你也可以自己去封装,毕竟这里的功能未必适合你的项目。下面是封装后预留的接口:
public interface ITextToAudio
{
event UnityAction<string> onError;
IEnumerator GetAudioClip(string text, UnityAction<AudioClip> OnGet, Params param = null);
IEnumerator Downland(string[] text,UnityAction<float> onProgressChanged ,Params param = null);
void CleanUpCatchs();
}
其中,下载的时候都是使用协程,可以利用WWW直接将得到的AudioClip返回回来(值得注意的是,如果你的音频足够了解应该可以直接从byte中创建audioClip,就没有必要使用www了,但目前也就暂时这样使用着)。而Params就是对官方参数的解析类,可以自行定义。基于官方的字符串结构,这样生成比较理想(直接重写ToString):
public override string ToString()
{
var fields = typeof(Params).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.GetField | System.Reflection.BindingFlags.Instance);
var param = new string[fields.Length];
for (int i = 0; i < fields.Length; i++)
{
param[i] = fields[i].Name + "=" + fields[i].GetValue(this);
}
return string.Join(",",param);
}