最近在做一个增强现实APP,为了方便3D处理使用了Unity作为开发工具。

 结果在实现语音识别功能时被卡住了,Unity上并没有语音识别的插件_(:3」∠)_。原本想来语音功能已经十分成熟,应该有很多工具可以调用,然而诸多公司有几乎全平台的SDK却没有Unity(明明只是请求服务器,偏偏都要封装)。

 好在最后找到了百度的rest方案,直接把web接口暴露给我们了,可以直接使用网络请求进行语音传输和识别,Unity的网络功能自然是没问题,现在4G普及各种环境网速也能跟上了。至此Unity原生语音识别理论上成立了。

 这篇文章大部分信息参考Unity官方文档,由于我上手Unity也不久,不可避免会产生谬误差错,欢迎指出,若有区别以官方为准。

 做语音识别首先需要获取语音,Unity中有现成的方法获取麦克风音频,往MainCamera上添加个AudioSource就可以接受麦克风的录制数据:

1
aud.clip = Microphone.Start(Microphone.devices[0], true, 600, 8000);

aud是AudioSource,clip是其中待播放的剪辑,Microphone.Start顾名思义,录音开始,包含四个参数:

  • 第一个是录音麦克风的名字。Microphone.devices可以获取当前麦克风名称的数组,我们只有一个麦克风所以直接获取了第一个的名字
  • 第二个是和第三个相关,第三个参数是录制长度,这里是10分钟。至于这么长时间和下半部分自动识别有关。第二个参数是个flag,选择是否循环录音,即录制满10分钟后将录音首部开始覆盖。
  • 最后一个是录音采样率,这里使用8000是百度语音识别API的要求

 现在语音数据已经在AudioSource里了,下一步就是把它发送给百度。
AudioSource.Clip有个GetData方法,可以将录音读出:

1
aud.clip.GetData(talk, recStart);

 它有两个参数,talk是导出数据的数组,需要预先设置大小;
recStart则表示导出数据的起始位置,我们的录音采样频率设置为了8000,则每秒数组会增大8000个float,你需要录音开始n秒以后的数据就是recStart=n*8000,这是后文自动识别的关键。

 数据格式是float[],范围[-1,1],这样的的非标准格式数据显然不能被用作识别,我们需要将之转换,Unity中录音数据转wav格式网上有不少,就不上代码了,也可以参考文末给出的demo。

 完成了转码后,就可以着手生成Json发送进行语音识别了,关于百度的API具体可参看百度语音文档中心

 使用百度API需要注册应用,并获取access_token,才会响应。注意百度的语音识别数据要求使用Base64编码。
Json结构:

1
2
3
4
5
6
7
8
9
10
11
postObj jsonObj = new postObj()
{
format = "格式,这里是wav",
rate = 8000,
channel = 1,
lan="语种zh、en等",
token = "你的access_token",
cuid = "识别码,可以自己随便填",
len=语音长度,单位byte,
speech=Base64后的语音数据
};

 成功后百度会返回如下格式

1
2
3
4
5
6
7
8
9
10
{
"corpus_no": "6281514336740510412",
"err_msg": "success.",
"err_no": 0,
"result": [
"soccer, ",
"a soccer, "
],
"sn": "933206695561462529026"
}

 至此Unity的语音识别功能已经实现了,但是Unity的录音函数录音长度是需要确定的,也需要触发,如果每次语音识别都需要用户开关,显然很麻烦,接下来我们来实现自动识别语音。

 要实现自动识别,就要判断用户何时在讲话,GetData的给出的float数据是每个音频采样点的响度,随着时间的推移组合出复杂的音频波形,对于我们来说可以简单设置采样阈值。在一段时间内,平均响度数据高于此则判断用户在说话,将录音输出,直到用户停止讲话。这个判断需要定时循环执行,对于Unity来说,可以直接使用update函数方便地实现。

在这里我们需要另一个MicroPhone方法:

1
currP = Microphone.GetPosition(mcName);

这个函数返回int数据表示录音进度当前的位置。

 在每个update内判断用户是否说话,要是开始说话就记录下当前时间为起始位置,进入录音状态,当用户结束讲话,记录下结束位置。而用户这次的语音数据就在Clip记录的录音数组中两个位置之间。

 至此实时识别已经初具雏形,我们还需要优化几个点

  • 判断时间的间隔不宜过短,会导致语音中自带的轻音,停顿被识别,语音被断开,导致失败。
  • 考虑到各类情况,需要在计算机判断的结果前后加上缓冲区,防止语音头尾被平均,不识别。

最后完善的实时识别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
   private int currP, lastP;
private int recStart;
public bool isRec = false, Speaking = false;
public float voiceLevel = 0.001f;
public bool isSpeak = false;
public float time;
public const float deltaT = 0.2f;
private int timeback = 0;

void Update () {
time -= Time.deltaTime;
if (time < 0)
{
time = deltaT;

if (isRec)
{
lastP = currP;
currP = Microphone.GetPosition(mcName);
int audioL = currP - lastP;
if (audioL <= 0)
return;
float[] tickAudio = new float[audioL - 1];
aud.clip.GetData(tickAudio, lastP);
float loudness = coculateLoud(tickAudio);
isSpeak = loudness > voiceLevel;
//Debug.Log(loudness);
if (!isSpeak)
{
if (Speaking)
{
timeback = 3;
Speaking = false;
}
}
else
{
if (!Speaking)
{
Speaking = true;
recStart = lastP-4000;
if (recStart < 0)
recStart = 0;
}
}
if (timeback-- == 1)
{
timeback = 0;
float[] talk = new float[currP - recStart - 1];
aud.clip.GetData(talk, recStart);
this.paly(talk);
}
}
}
}

最后本例的demo地址Unity-Realtime-SpeechRecognition