Windows平台下VoIP语音引擎的框架设计 2010-10《电信工程技术与标准化》
陈亮蒿1, 2 李炜1, 2
(1 北京邮电大学网络与交换技术国家重点实验室北京100876;
2. 东信北邮信息技术有限公司北京 100191)
摘要:VoIP (Voice over IP)语音引擎,封装了语音编解码等一系列数据处理,可以快速集成至VoIP客户端,从而构成终端产品。基于Windows平台,本文主要讨论了录放音技术、回声消除技术,并对延迟抖动做了处理,以满足用户对语音质量的需求。
关键字: VoIP 录放音回声消除 延迟抖动
Design of VoIP Voice Engine Framework on Windows Platform
Chen Lianghao1, 2 Li Wei1, 2
(1. State Key Laboratory of Networking and Switching Technology, Beijing University of Posts and Telecommunications, Beijing 100876;
2. EBUPT Information Technology Co. Ltd., Beijing 100191)
[Abstract] VoIP (Voice over IP) voice engine, processing a series of data such as voice encoding and decoding, can be quickly integrated to the VoIP client, forming a terminal product. Based on Windows platform, the paper discussed audio recording and playing, AEC (Acoustic Echo Cancellation), and processed delay jitter to meet the users’ needs of voice quality.
[Key words] VoIP, Audio Recording and Playing, AEC, Delay Jitter
1 引言
VoIP (Voice over IP) [1]的基本原理是由专门设备或软件将呼叫方的语音信号采样并数字化、压缩、转换为一定长度的数字化语音包,以数据包的形式经过分组交换网络传输到对方,对方接收到数据包后解压缩,还原成语音信号。它提供了低成本、高灵活性、高效率的增强应用。IP在硬件、软件和网络协议等方面的提高和发展,进一步推动了新的集成化基础设施的迅速发展[2]。
随着PC、手机、宽带网越来越普及,VoIP应用越来越广泛,不断影响着人们的工作生活。基于Windows平台的VoIP客户端无疑是应用最广泛的,如QQ、MSN、Skype等等。但Windows的版本众多,各个版本提供的API (Application Programming Interface)也不尽相同,开发时稍不注意就会严重影响语音质量。
本文提出了一套Windows平台下开发语音引擎的解决方案[3],有效解决了不同版本下引擎的兼容问题,使得引擎在不同Windows版本下能够体现出各自音频处理的优势。利用系统级API,讨论了Windows XP、Vista、Windows 7下录放音技术、回声消除技术的实现,并且通过算法设计有效解决了延迟抖动问题。
2 总体结构
VoIP引擎的总体结构主要包括两个部分:处理音频的核心控制模块和处理本地文件的文件播放模块。基于这两个核心模块,可以在上层封装出各种对外功能接口,例如针对C++的DLL (Dynamic Link Library),或者针对Java、C#等的应用接口。系统总体结构如图1所示。
图1 系统总体结构
1)本地文件播放模块:负责对本地的媒体文件进行操作,如播放某文件、停止播放、是否循环播放等等。
2)引擎核心控制模块:通过调用下层模块的功能接口,进行相关数据逻辑处理,控制数据流以及播放录制缓存等。而且,它对上层也提供了其调用接口及回调接口。下层模块主要包括文件音频模块、会话模块、设备音频模块、音频编解码模块。
3)文件音频模块:负责音频文件作为媒体传输源时的相关控制及信息保存,包括数据分发列表(一个音频源在同一时刻对应几个会话)、音频文件的文件路径、文件中存储的音频编解码格式等。
4)会话模块:负责一路会话的描述。一路会话对应一个RTP模块,并且保存会话的其它相关信息,如当前数据源是麦克风输入还是某个媒体文件、语音的收发模式(同时收发、只收不发、只发不收、不收不发)、音频编解码格式等等。
5)设备音频处理模块:包括音频的录制、播放,音频设备的管理,以及音量的控制等。待播放的数据从引擎核心控制模块中获取,已录制的数据也直接交给引擎核心控制模块进行处理。
6)音频编解码模块:负责各种音频编码格式的相互转换。将录制的音频转换为指定编码格式进行传输,对接收到的音频数据进行解码等。
3 核心技术
3.1录放音技术
3.1.1 Windows XP系统
DirectSound是一套基于COM (Component Object Mode)接口的Windows应用层音频控制API,在Windows XP及其以前版本,作为标准的音频应用程序开发接口提供给开发者。底层直接连接驱动,有些底层接口直接由硬件实现,运行效率很高。
DirectSound的功能包括音频设备的控制、音量控制、录放音。我们关心的录放音也分为三种:播放、录音和录放双工。如果要实现回声消除(Acoustic Echo Cancellation, AEC),必须采用第三种录放双工的方式进行初始化。对应的初始化函数分别为:DirectSoundCreate8、DirectSoundCaptureCreate8、DirectSoundFullDuplexCreate8。从应用效果看,这三个函数的效果就是获得两个接口:IDirectSound8和IDirectSoundCapture8。其中,DirectSoundCreate8获取IDirectSound8,DirectSoundCaptureCreat8获取IDirectSoundCapture8,而DirectSoundFullDuplexCreate8则是同时获取两个接口。
获取总体接口后,用DirectSoundCreate8函数初始化的程序必须调用SetCooperativeLevel函数设置播放的优先级,否则后续步骤会失败。
然后,要初始化播放或录音的Buffer。初始化时,将播放音的属性、音频格式、缓存大小、特效标识等信息作为参数,在初始化函数调用时告诉DirectSound。具体设置哪些值和如何设置,请参看微软DirectSound帮助文档[4]。
DirectSound录放音有两种形式:一种是静态缓存,即一次性把要播放的音频数据导入缓存;另一种是流模式,即播放录制过程中会不断有数据补充交互,缓存会被复用。对于我们的应用,主要关心流模式。基于流模式,DirectSound提供了Buffer通知模型,即通过设置函数,播放或者完成录制多少字节的Buffer后,就发送一个Event事件。外部线程可以拦截响应这个事件,对Buffer的数据进行处理,实现连贯的平滑的录放音。方法是通过IDirectSoundBuffer8或者IDirectSoundCaptureBuffer的QueryInterface方法获取IDirectSundNotify8接口,然后调用该接口的SetNotificationPositions方法,设置通知事件和事件通知点。
由于CPU是被抢占运行的,因此,通知时一般都有微小时间滞后。所以,事件触发后的处理,如果有对缓存的精确处理,需要重新获取缓存状态信息。之后,就可以调用Buffer的Start函数,开始录音或者放音;调用Buffer的Stop函数可以立即结束录音或者放音。
3.1.2 Vista与Windows 7系统
Vista和Windows 7在音频播放录制方面提出了一套全新的概念和架构。在设备上,微软取消了声卡的概念,取而代之的是播放设备、麦克风和线路输入设备。Vista和Windows 7包含了多套应用层音频API,有的是老版本Windows的API,如WaveXxx和DirectSound,有的是新的,如Media Foundation中的SAR。在这些应用层之下,微软还开放了一套层次较低的公共API——Core Audio APIs[4],用于一些实时性较高或者对设备控制有较高需求的应用,如图2所示。
图2 Vista和Windows 7操作系统的音频处理架构
在Vista及以后版本,DirectSound不再作为微软主流音频应用接口,但微软在Vista和Windows 7上仍然保留了DirectSound的大多数接口,使得一些老程序可以运行。但是,该接口变成一套软件接口进行维护,因此有些程序在Windows XP下声音很流畅,而在Vista和Windows 7下却不好,因此这里讨论的录放音技术是直接基于Core Audio APIs。
由于Core Audio APIs的定位是针对音频设备的操作,而并不包括对音频的处理。因此,我们的应用还需要自己手动加入音频处理相关的模块,如重抽样和AEC等,微软提供了Digital Signal Processor (DSP)系列接口对音视频等进行处理[4]。
录音的总体流程:
1)Core Audio APIs调用录制音频API,初始化并开始录音;
2)通过流操作,定期获取录制的音频数据;
3)将数据交给DSP的重抽样接口,变换为我们需要的格式的数据;
4)将重抽样处理过的数据传递给DSP的AEC回声消除接口;
5)将经过回声消除处理过的数据交给应用程序处理。
这个流程是标准的流程,不过DSP的AEC接口还提供了Source Mode类型接口,将前四个步骤合并为一个接口,大大简化了操作。简化后的录音流程为:
1)启用并初始化DSP的AEC接口,模式要选择Source Mode;
2)用AEC接口开始录音,并定期取出数据交给应用程序处理。
放音的总体流程:
1)Core Audio APIs调用播放音频API,初始化并获得接口对象;
2)初始化并设置好重抽样DSP;
3)获取并锁定播放缓冲,向其中填充通过重抽样处理过之后的数据,并解锁缓冲;
4)开始播放;
5)另起线程,定期重复第三步,补充音频数据,直到终止播放。
3.2回声消除技术
3.2.1 Windows XP系统
在Windows XP操作系统下,微软提供了基于DirectSound的AEC解决方案。在Windows XP下启用AEC,必须在DirectSound初始化时,采用DirectSoundFullDuplexCreate8函数,及必须以双工的方式进行初始化,将播放缓冲、录制缓冲及音频格式等信息作为参数传进去。注意结构体DSCBUFFERDESC的dwFlags一定要包含DSCBCAPS_CTRLFX标识,并且DSCEFFECTDESC一定要做如下初始化,并把指针传给DSCBUFFERDESC的lpDSCFXDesc。
// 参数:施加于录制缓冲区的效果描述,AEC & NS
DSCEFFECTDESC dsced[2];
ZeroMemory( &dsced[0], sizeof( DSCEFFECTDESC ) );
dsced[0].dwSize = sizeof(DSCEFFECTDESC);
dsced[0].dwFlags = DSCFX_LOCSOFTWARE;
dsced[0].guidDSCFXClass = GUID_DSCFX_CLASS_AEC;
dsced[0].guidDSCFXInstance = GUID_DSCFX_MS_AEC;
dsced[0].dwReserved1 = 0;
dsced[0].dwReserved2 = 0;
ZeroMemory( &dsced[1], sizeof( DSCEFFECTDESC ) );
dsced[1].dwSize = sizeof(DSCEFFECTDESC);
dsced[1].dwFlags = DSCFX_LOCSOFTWARE;
dsced[1].guidDSCFXClass = GUID_DSCFX_CLASS_NS;
dsced[1].guidDSCFXInstance = GUID_DSCFX_MS_NS;
dsced[1].dwReserved1 = 0;
dsced[1].dwReserved2 = 0;
// 参数:录制缓冲区描述
DSCBUFFERDESC dscbd;
dscbd.dwSize = sizeof(DSCBUFFERDESC);
dscbd.dwFlags = DSCBCAPS_CTRLFX;
dscbd.dwBufferBytes = m_dwBlockNum * m_dwBlockSize ;
dscbd.dwReserved = 0;
dscbd.lpwfxFormat = &m_wfxRecord;
dscbd.dwFXCount = 2;
dscbd.lpDSCFXDesc = dsced;
然后,将DSCFXAec结构体中的dwMode设置为DSCFX_AEC_MODE_FULL_DUPLEX,并通过录音接口的函数进行设置。后面的步骤和一般DirectSound的录放音步骤相同,先分别设置缓存的上报通知事件点,然后初始化并填充播放缓存,开始播放,开始录制等。详情请参看微软DirectSound帮助文档[4]。
3.2.2 Vista与Windows 7系统
在Vista和Windows 7操作系统中,微软提供了DSP系列接口对音视频等进行处理。其中,和音频相关的处理,包括AEC和重抽样Resample。尽管Windows 7较Vista有不少新的功能和应用接口,不过它们共用的接口(Core Audio APIs)已经能够满足我们的应用需求。
实现AEC的过程如下:首先通过调用COM组件的方法,通过CoCreateInstance函数获取CLSID_CWMAudioAEC的组件接口IMediaObject。再通过该接口的QueryInterface函数,获取IPropertyStore接口。通过IPropertyStore接口的SetValue函数,将MFPKEY_WMAAECMA_SYSTEM_MODE属性的值设为SINGLE_CHANNEL_AEC,即采用单声道AEC模式。其他的属性可根据用户需要,分别设置。之后,通过IMediaObject接口的SetOutputType函数,用DMO_MEDIA_TYPE结构体作为参数传入,设置输出的音频格式。然后调用IMediaObject接口的AllocateStreamingResources函数开始录制。详情请参看微软Windows SDK文档及Demo程序[4]。
3.3操作系统版本判定
对不同的操作系统版本编写设备音频处理模块,并封装成不同的类,类之间无耦合关系。同时,提取出公共接口,与核心控制模块交互。
通过操作系统提供的系统API获取当前操作系统的版本,并根据相应的版本,初始化对应的设备音频处理模块。辨别操作系统版本的API为GetVersionEx,该API支持的最低版本的操作系统为Windows 2000,具体用法详见微软MSDN帮助文档[4]。常见Windows操作系统版本如表1:
表1 常见Windows操作系统版本
4延迟抖动消除算法
IP网络的一个特征就是网络延迟与抖动,这将导致IP电话音质下降。网络延迟是指1个IP包在网络上传输所需的时间,抖动是指IP包传输时间的长短变化。如果抖动较严重,那么有的语音包会因迟到而被丢弃,产生语音的断续及部份失真,严重影响音质[5]。为了降低或者消除抖动的影响,我们采用缓冲技术,即在接收方设定1个缓冲区,语音包到达时首先进入缓冲池暂存,系统以稳定平滑的速率将语音包从缓冲池中取出、解压、播放给收话者。这种缓冲技术可以在一定限度内有效处理语音抖动,提高音质。接下来,我们将以消除或者降低延迟抖动的影响从而平滑语音流、提高语音质量为目的,分别针对发送端与接收端进行探讨。
4.1发送端缓存控制
在发送端,需要稳定地发送数据包。由于发送端的声卡自身的定时器不一定准确,可能偏快或偏慢:若偏快,将导致在单位时间内发送的数据包比预期的多,时间一长,接收端缓存而不能及时处理的数据包越来越多,最终导致播放延迟越来越大,即延迟扩大。如PC终端呼叫手机或固定电话时,即会发生如此情况,因此,我们必须在发送端控制数据包的平稳发送,避免延迟扩大。
首先,采用比较准确的定时器(如CPU定时器);然后设置一个相对较长的计数周期t(时间较短则相对误差较大,无可信性),如10s,计算出在该时间段内理论上应发出的数据包个数a。(其中,若8KHz的采样率,1个数据包含160个采样,表明1s发送50个数据包,每20ms发送1个数据包)
在发包过程中,同时做如下处理:
Step1:在第n个计数周期内,通过计数器计算出实际向外发送的数据包个数bn。若n=1,转Step2;否则转Step3。
Step2:若bn>a,即发包速率偏快,记录cn=bn-a;否则记录cn=0。转Step5。
Step3:若cn-1>0,则在当前计数周期内均匀丢弃cn-1个数据包,即每隔t/cn-1时间丢弃1个数据包。转Step4。
Step4:若bn-a+cn-1>0,记录cn=bn-a+cn-1;否则记录cn=0。转Step5。
Step5:n++。转Step1。
流程图参见图3所示:
图3 发送端数据包的缓存控制
这样就能控制发送端的数据包实际速率与理论速率达到动态平衡,防止传输过多的数据包,避免延迟扩大。因丢弃包是均匀的,用户感觉不到这微小的差异,故此方案可行。
4.2接收端缓存控制
在接收端,需要平滑地读取数据包。这就需要一个缓存,来存放并整理刚来到的数据包,在其达到一定数量时,再取出相应数据流畅地播放。如若没有缓存,来一数据就开始播放,极有可能出现语音播放不流畅,先发送的数据包也有可能后到达,这样的播放就会出现逻辑混乱的错误。
在收包的过程中,需要做如下处理: