那在现实生活中,微信的语音功能让人铭记于心。手指按下录音,抬起发送。
试想一下,如果某些场景,我们在没有手的情况下如何让设备进行识别。
今天,我将我自己封装的语音打断的代码共享给大家,各位可以按自己的需求和场景需要修改代码细节。
原理介绍:
启动一个新线程 ←←←←←←←←←←←←←←←←←←←←
↓ ↑
开始录制(聆听) ↑
↓ ↑
当前音量低于从录音开始的平均音量多于设定阈值 ↑
↓ ↑
结束录制,生成文件并上传至百度进行语音识别(聆听)→→→
↓
语音识别返回结果
↓
将结果丢到下一个逻辑
↓
线程销毁
依照以上的逻辑,我们可以在线程不间断的对接下识别所有的句子。各位可以调整设定阈值来控制打断时机。
这里安利一下上几章节给各位讲到的windows录音类。
《[语音技术]C#在windows平台的录音类封装》 url:http://ai.baidu.com/forum/topic/show/492634
《[语音技术]C#在win平台基于录音类试写唤醒》 url:http://ai.baidu.com/forum/topic/show/492635
有要用UNIT的同学可以看一下c#调用UNIT通用API的文章
《[UNIT] C#利用API调用UNIT》 url:http://ai.baidu.com/forum/topic/show/492630
关于UNIT配置 请移步至UNIT板块,有我详细的3篇配置UNIT的经验贴 这里就不说了
这里我在上一章节的基础上再次封装一个类,目的是为了更好的控制识别流程,是根据我项目场景来的。我命名为了SoundListener
class SoundListener
{
private string time; //文件名
private bool IsChecking;//是否在识别中
private SoundRecord sr; //录音类
private int VolumnCount; //音量计数器
private readonly Asr _asrClient; //ASR SDK
public static bool IsJarvis; //判断是否触发唤醒
public SoundListener(Asr asr,bool isjarvis) //构造函数
{
time = DateTime.Now.ToString("yyyyMMddHHmmss"); //获取时间
IsChecking = true; //开始录制信号
sr = new SoundRecord(); //new一个录音类
sr.SetFileName(time + ".wav"); //设置文件名
_asrClient = asr; //创建语音识别
IsJarvis = isjarvis; //继承唤醒信号
}
public Task Start() //核心方法 往后看 会再介绍
{
return Task.Run(() =>
{
sr.RecStart();
while (IsChecking)
{
if (sr.CurrentVolume < sr.AverageVolumn)
{
VolumnCount++;
Thread.Sleep(10);
}
else
{
VolumnCount = 0;
}
if (VolumnCount >= 100)
{
VolumnCount = 0;
Console.Write(".");
sr.RecStop();
//Thread.Sleep(50);
IsChecking = false;
}
}
});
}
public Task Update() //上传
{
return Task.Run(() => //Task线程
{
var data = File.ReadAllBytes(time + ".wav"); //读文件
Dictionary d = new Dictionary(); //asr接口参数
//d.Add("lan", "zh"); //指定中文识别
var result = _asrClient.Recognize(data, "pcm", 16000, d); //开始识别
if (result.GetValue("err_msg").ToString() == "success.") //如果识别成功
{
File.Delete(time + ".wav"); //文件删除
Application.Current.Dispatcher.Invoke(() => //再开一个线程
{
string res = result.GetValue("result").First.ToString(); //拿到识别结果的第一个(接口可能会返回多个结果,但机器不会判断哪个好用,好吧应该能判断,我写不来,所以我默认第一个了)
Console.WriteLine(result.GetValue("result").ToString()); //将识别结果打印出来 以便可以查看识别准确率
Regex regex = new Regex("贾维斯"); //我自己的唤醒词
Match match = regex.Match(res); //与识别结果去匹配
if (match.Success || IsJarvis) //说出了贾维斯 或 处于唤醒状态 开始一系列操作
{
if(IsJarvis) //处于贾维斯
{
if (match.Success) 说了贾维斯 进入睡眠逻辑
{
IsJarvis = false;//退出贾维斯模式
Console.Write("\t进入睡眠,等待唤醒");
}
else //没说贾维斯,开始处理识别结果
{
if (!UNIT.UNIT.IsFinishedThisUnit) //UNIT未结束意图
{
Queue.Queue.WaitForIntentionsWord.Add(WordToNumber.WordToNumberClass.WordToNumber(res));//文字转数字后添加到意图澄清队列(我自己写的消息队列,比较蠢 这里是词槽澄清的逻辑 不用UNIT的可以忽略)
}
else //UNIT返回的意图是satisfy
Queue.Queue.WaitForDealFromVoice.Add(WordToNumber.WordToNumberClass.WordToNumber(res));//文字转数字后添加到待意图识别队列(我自己写的消息队列,比较蠢 这里是UNIT意图识别完成返回执行函数的逻辑 不用UNIT的可以忽略)
}
}else //不处于唤醒状态时说了贾维斯
{
IsJarvis = true; //进入唤醒模式
Console.Write("\t唤醒成功");
Queue.Queue.WaitForDealFromVoice.Add(WordToNumber.WordToNumberClass.WordToNumber(res));//res..Replace(",", "").Replace("贾维斯", "")) //将唤醒词去掉后 文字转数字 丢入待意图识别队列
}
}
});
return result.GetValue("result").First.ToString().Replace(",",""); //方法执行成功返回语音识别的第一句
}
return null; //分析失败返回空
});
}
}
那这里介绍那个Start方法 如下:
private int VolumnCount;
public Task Start() //声明了一个Task方法 (Task贼好玩 有兴趣可以自行了解一下)
{
return Task.Run(() => //使用Task类 开启一个新的线程
{
sr.RecStart(); //开始录制
while (IsChecking)
{
if (sr.CurrentVolume < sr.AverageVolumn) //当前音量小于平均音量
{
VolumnCount++;
Thread.Sleep(10); //睡眠10毫秒
}
else //如果当前音量大于平均音量了 清空检测数值重头计算
{
VolumnCount = 0;
}
if (VolumnCount >= 100) //当抵达100*10=1000毫秒时 进入打断逻辑
{
VolumnCount = 0;
Console.Write("."); //输出信号,为了调试的时候让自己知道打断了
sr.RecStop(); //停止录制
IsChecking = false; //跳出这次死循环
}
}
});
}
以下是整个打断的调用方式:
bool IsJarvis = false;
private void Window_Loaded(object sender, RoutedEventArgs e)
{
//其他逻辑......
//语音识别线程
Application.Current.Dispatcher.Invoke(async () => //开启一个新的线程,以防止处理过程阻塞到UI线程 因为线程内会使用到异步等待方法,所以需要加上async关键字
{
while (true) //陷入死循环 因为我的设定是不停的听。所以不考虑出循环
{
SoundListener sl = new SoundListener(_asrClient,IsJarvis); //继承自上一次的唤醒状态新建对象
await sl.Start(); //调用SoundListener.Start方法,一直会阻塞到录制完成
Task t = new Task(async () =>//同样有异步等待的需求,加async关键字
{
string res = await sl.Update(); //上传 并等待上一次的执行结果 阻塞到识别逻辑全部结束
IsJarvis = SoundListener.IsJarvis; //将唤醒标识从SoundListener类中取出 然后Task自己会被辣鸡处理 因为第一次唤醒会默认执行,所以不考虑第一次的识别 之后的识别也会如期丢入UNIT识别
});
t.Start(); //Task类会一直存活,直到全部处理完成
}
});
//其他逻辑.......
}
主要的多线程循环识别的实现方式就是这样啦。可能会有点难喔 各位可以简化我的代码(因为我实在懒的去抠主逻辑了 因为我自己的代码都乱的看不下去)
我是自己写着玩的。。。。只负责实现= = 性能啥的我都没在意
有东西啊,大哥。我现在可以录音了,微软也是欺负人,wpf就配置的好好的,winform连个app.config都要自己配,真是烦死了。
另外想问,这响应速度你们接受么?如果要实现实时的把你的语音识别出来,有什么优化方案么?
你先不开线程,试试能正常录正常结束吗。打断点看看有没有异常可以分析
并没有任何东西。。。。
请问你的app.config文件是怎么设置的?查了一下资料,似乎跟app.config设置有关。
可能是吧,我当初也是到处搜,最后弄到了我那环境可用的然后改的
https://zhidao.baidu.com/question/74661123.html
另外想问一下大佬,上面是不是那个SoundRecord类的出处?我在百度知道搜到的。
Application.Current.Dispatcher.Invoke
我查了一下,winform是没有current方法的,wpf好像才有。
没有,你是用wpf的吧?我用的C# winform编程,不知道winform的dispatch.invoke怎么写。
= =呃。Form有没有dispatcher.Invoke
程序打断点都没用,真是。。。
另外CPU占用率很高
还是很奇怪,因为我这里已经把线程声明为窗体级变量了,设置的断点还是没能进入线程函数里面。真是头疼,不知道哪里出了问题。
是喔,没有把线程定义为全局变量,真是个低级的错误啊。感谢兄台的解答!
按钮事件结束了。。。我估摸着。因为你那线程是局部变量声明在了按钮单击事件里。可能单击完了就垃圾回收了
昨天调试的时候,把vs2017的开发环境都搞挂了。然后我跟踪了一下,发现线程里面装的函数根本就没有执行下去。请问线程提前死的原因是啥?
看着好像没问题,你检查下这线程是不是提前死了
兄弟,你看下我的代码,为何不能录制出WAV文件?
客气客气,互相学习
过来膜拜大佬 =0=
哎呀 流程图排版有问题 我用画图画了一张,各位凑合理解一下