基础设施建设的第四部分来到了语音转文字STT部分,这一部分仍然先使用科大讯飞的模型(主要是讯飞开放平台已经配过了)。
准备部分
UI准备
准备好语音输入模式的UI,配置鼠标事件,在点击语音输入按钮后需要切换文本输入框和语音输入框的状态,中间框起来的是UI的基本结构,右侧基本上操作的也是这些部分。
API准备
参考上一篇,依然是讯飞开放平台控制台-讯飞开放平台,选择语音识别——语音听写(流式版),只要没有切换应用,Websocket服务接口认证信息和上一部分TTS就用的是同一块,然后记下来API地址。
配置部分
对话控制器处理
private void Awake()
{
m_CommitMsgBtn.onClick.AddListener(delegate { SendData(); });
RegistButtonEvent();
}
在Awake部分增加按钮事件注册
private void RegistButtonEvent()
{
if (m_VoiceInputBotton == null || m_VoiceInputBotton.GetComponent<EventTrigger>())
return;
EventTrigger _trigger = m_VoiceInputBotton.gameObject.AddComponent<EventTrigger>();
//添加按钮按下的事件
EventTrigger.Entry _pointDown_entry = new EventTrigger.Entry();
_pointDown_entry.eventID = EventTriggerType.PointerDown;
_pointDown_entry.callback = new EventTrigger.TriggerEvent();
//添加按钮松开事件
EventTrigger.Entry _pointUp_entry = new EventTrigger.Entry();
_pointUp_entry.eventID = EventTriggerType.PointerUp;
_pointUp_entry.callback = new EventTrigger.TriggerEvent();
//添加委托事件
_pointDown_entry.callback.AddListener(delegate { StartRecord(); });
_pointUp_entry.callback.AddListener(delegate { StopRecord(); });
_trigger.triggers.Add(_pointDown_entry);
_trigger.triggers.Add(_pointUp_entry);
}
当用户按下按钮时调用 StartRecord() —— 开始语音录制,当用户松开按钮时调用Stop()按钮——结束语音录制。
这里需要特别说明一下Unity的监听和委托机制:
1.首先创建监听器_trigger监听条目,这里EventTrigger.Entry 是一个监听“条目”,代表一个具体事件类型的监听器。
2.通过设置监听器eventID 指定监听哪种事件,比如 PointerDown(按下)、PointerUp(松开)等。
3.定义回调事件,callback 是一个 UnityEvent,即事件响应函数的集合,首先让它完成初始化,再向其中添加委托事件。
4.最后将监听器添加到EventTrigger 的 triggers 列表,完成注册。
StartRecord()与StopRecord()如下:
public void StartRecord()
{
m_VoiceBottonText.text = "正在录音中...";
m_VoiceInputs.StartRecordAudio();
}
public void StopRecord()
{
m_VoiceBottonText.text = "按住按钮,开始录音";
m_RecordTips.text = "录音结束,正在识别...";
m_VoiceInputs.StopRecordAudio(AcceptClip);
}
这里使用的VoiceInputs是子组件录音器,这里负责调用麦克风录音并存储录音文件。
public void StartRecordAudio()
{
recording = Microphone.Start(null, false, m_RecordingLength, 16000);
}
/// <summary>
/// 结束录制,返回audioClip
/// </summary>
/// <param name="_callback"></param>
public void StopRecordAudio(Action<AudioClip> _callback)
{
Microphone.End(null);
_callback(recording);
}
回到对话控制器,这里使用了回调机制将函数 AcceptClip()作为参数传过去,以便于在完成StopRecordAudio之后直接进行音频片段处理。
private void AcceptClip(AudioClip _audioClip)
{
if (m_ChatSettings.m_SpeechToText == null)
return;
m_ChatSettings.m_SpeechToText.SpeechToText(_audioClip, DealingTextCallback);
}
这里具体的处理开始调用设置的STT组件,也就是准备阶段科大讯飞部署的部分,将音频转换为相应的文字,而在完成处理之后直接回调DealingTextCallback以继续进行对话部分。
private void DealingTextCallback(string _msg)
{
m_RecordTips.text = _msg;
StartCoroutine(SetTextVisible(m_RecordTips));
//自动发送
if (m_AutoSend)
{
SendData(_msg);
return;
}
m_InputWord.text = _msg;
}
TTS类部分
从对话控制器调用过来,转换完音频数据类型之后启动了Unity协程来异步发送音频数据。
public override void SpeechToText(AudioClip _clip, Action<string> _callback)
{
byte[] _audioData = ConvertClipToBytes(_clip);
StartCoroutine(SendAudioData(_audioData, _callback));
}
public IEnumerator SendAudioData(byte[] _audioData, Action<string> _callback)
{
yield return null;
ConnetHostAndRecognize(_audioData, _callback);
}
在协程中通过websocket启动与讯飞服务器的连接,发送音频数据,并解析返回的语音识别结果,这里参考了API的使用文档。
private async void ConnetHostAndRecognize(byte[] _audioData, Action<string> _callback)
{
try
{
stopwatch.Restart();
//建立socket连接
m_WebSocket = new ClientWebSocket();
m_CancellationToken = new CancellationToken();
Uri uri = new Uri(GetUrl());
await m_WebSocket.ConnectAsync(uri, m_CancellationToken);
//开始识别
SendVoiceData(_audioData, m_WebSocket);
StringBuilder stringBuilder = new StringBuilder();
while (m_WebSocket.State == WebSocketState.Open)
{
var result = new byte[4096];
await m_WebSocket.ReceiveAsync(new ArraySegment<byte>(result), m_CancellationToken);
//去除空字节
List<byte> list = new List<byte>(result); while (list[list.Count - 1] == 0x00) list.RemoveAt(list.Count - 1);
string str = Encoding.UTF8.GetString(list.ToArray());
//获取返回的json
ResponseData _responseData = JsonUtility.FromJson<ResponseData>(str);
if (_responseData.code == 0)
{
stringBuilder.Append(GetWords(_responseData));
}
else
{
PrintErrorLog(_responseData.code);
}
m_WebSocket.Abort();
}
string _resultMsg = stringBuilder.ToString();
//识别成功,回调
_callback(_resultMsg);
stopwatch.Stop();
Debug.Log("讯飞语音识别耗时:" + stopwatch.Elapsed.TotalSeconds);
}
catch (Exception ex)
{
Debug.LogError("报错信息: " + ex.Message);
m_WebSocket.Dispose();
}
}
private string GetWords(ResponseData _responseData)
{
StringBuilder stringBuilder = new StringBuilder();
foreach (var item in _responseData.data.result.ws)
{
foreach (var _cw in item.cw)
{
stringBuilder.Append(_cw.w);
}
}
return stringBuilder.ToString();
}
private void SendVoiceData(byte[] audio, ClientWebSocket socket)
{
if (socket.State != WebSocketState.Open)
{
return;
}
PostData _postData = new PostData()
{
common = new CommonTag(m_XunfeiSettings.m_AppID),
business = new BusinessTag(m_Language, m_Domain, m_Accent),
data = new DataTag(2, m_Format, m_Encoding, Convert.ToBase64String(audio))
};
string _jsonData = JsonUtility.ToJson(_postData);
//发送数据
socket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes(_jsonData)), WebSocketMessageType.Binary, true, new CancellationToken());
}
关于协程
由于我们在Unity中一般不考虑多线程,所以要使用协程。
协程,从字面意义上理解就是协助程序的意思,我们在主任务进行的同时,需要一些分支任务配合工作来达到最终的效果。
稍微形象的解释一下,想象一下,在进行主任务的过程中我们需要一个对资源消耗极大的操作时候,如果在一帧中实现这样的操作,项目就会变得十分卡顿,这个时候,我们就可以通过协程,在一定帧内完成该工作的处理,同时不影响主任务的进行。
协程和线程之间是有异有同,对于协程而言,同一时间只能执行一个协程,而线程则是并发的,可以同时有多个线程在运行。但两者在内存的使用上是相同的,共享堆,不共享栈。