markdown写出来怎么这么丑啊!(。˘•ε•˘。)
写在前面
一开始是打算用这个老接口做讯飞语音识别的程序,在转移到UWP时发现,这玩意在Windows Runtime中屏蔽(弃用)了,将来会更新使用WASAPI的程序
WaveRecorder类代码下载地址
录音无非两种需求:
1. 非实时获得音频,也就是停止录音了你才需要处理它;
2. 实时获取音频,比如QQ电话这种这边讲话那边马上就听到的。
后者实现起来比较啰嗦,但也很定式。既然啰嗦那就乖乖地写成类吧,管别的大仙怎么说你low呢 ( ͡° ͜ʖ ͡°) 。
首先你要知道…
Windows API 可用的实现方式 | 用处 |
---|---|
Windows Multimedia API(WaveXxxAPI) | 定式且啰嗦地实现音频流的实时获取。 |
Media Control Interface (MCI) | 让你简单地用字符串命令实现录音和放音 |
——MMAPI可以让你访问音频设备的缓冲区,发挥比较自由,接近系统底层。
——MCI是发送字符串指令给这个界面,内部的无法干涉,记得《少年电脑世界》之类的杂志教你打开关闭光驱什么的就是用写一行
mciSendString("set cdaudio door open",NULL,0,NULL);//关闭把open改为close
。
注意:MCI简称媒体控制接口,较为高层,为的是让你不用关心设备的具体操作,快速上手,简单地操作多媒体设备。WaveXxxAPI是接近底层的应用程序接口,为的是灵活地控制设备,但设备的操作还是比较定式的,灵活在于设备状态配置和数据处理的时机,所以M$给你了两个控制方式。
下面我们来看看如何用这两种方式实现录音:
使用MMAPI(WaveXxxAPI)
怎么和音频流打交道
用MMAPI的估计都是想实时获得音频数据的,MMAPI可以把音频流缓冲起来并一块一块地发送给你,在这里我们暂把这种固定大小的音频裸数据简称为AudioFrame(数据块,代码中别名叫ChunkData)。这一块数据需要你一次性处理完(你甚至需要转移这个数据到另一个线程以保证缓冲区读写的稳定性),数据有多少字节可以根据实际情况来设定。在性能和延迟之间均衡考虑一下,200ms的数据可以应付大多数情况。
PS:用过Kinect V2麦克风的同学对此应该比有印象,AudioFrame顾名思义,音频帧。
操作流程
MMAPI的操作十分定式:
开始录音的流程为:以一种格式打开波形输入设备,发送WIM_OPEN消息给回调函数,准备缓冲区,添加缓冲区到设备,告诉设备录音开始;
录音期间循环发送WIM_DATA给回调函数;
结束录音的流程为:告诉设备录音结束并发送WIM_DATA给回调函数让它处理最后的数据,重置录音设备,释放缓冲区,至此可以重新设置缓冲区到设备并开始新的录音,关闭设备并发送WIM_CLOSE给回调函数。
关于MMAPI的回调函数:
这个回调函数是来处理消息的,一开始收到WIM_OPEN,最后收到WIM_CLOSE唯一频繁收到的消息是WIM_DATA,得到这个消息时我们需要转移缓冲区里的数据并把缓冲区压入到设备缓冲队列中,你可以理解为自动pop手动push。
我写的类里面的回调函数是属于这个回调函数的,阻塞的话还是会直接影响MMAPI的回调函数
这里用到双缓冲乃至多缓冲技术:
假设一个实时接水的任务,听起来奇怪但与MMAPI的处理流程相似,这里需要你用杯子连续接水,杯子相当于你开辟的缓冲区:
根据上面的流程,你的身份是MMAPI的回调函数,在饮水机面前要拿着杯子执行这个任务:任务的基本指标是滴水不漏地连续用杯子把水接到一个存储区域里,你要接指定量的水,还要负责转移走这杯水。为了能腾出时间把水倒在存储区域里,你肯定需要用不止一个杯子轮流接水。饮水机有一个功能:每当杯子灌满后,饮水机会通知你,并自动去接下一个杯子,如果后面没有杯子则终止任务。(不会讲故事的我啊TT,这个奇葩例子能看懂就行)可以见得:1.你会被及时地通知去转移数据2.缓冲区用完了要及时放回缓冲区队列后端以保证任务能够继续3.如果转移并处理数据的时间不是很稳定,你可能需要准备多个缓冲区而不是单纯增加缓冲区容量,为的是确保任务中能够预留足够多的容忍时间供你使用。
这里的多缓冲技术浅显地解释就是多个缓冲区排成一个队列(或者理解为放成一摞)来抵消这个任务中那些耗时不稳定的处理过程对整个实时处理任务的连续性带来的负面影响。其实生活中有很多事情也是用到多缓冲这个概念。
代码
这一部分我把自己写的类里面的函数拿了出来,完整代码请见链接,免下载积分。
需要添加:
#include "mmsystem.h"
// using namespace std;
#pragma comment