编者注:
虽然现在Microsoft说Windows低级音频函数已经obsoleted,DirectSound也已经大行其道,但其相对易用的特点还是值得学习的。本文的另一个特点是作者结合了局域网实时音频传输的课题给出了这些API函数的使用实例,使得全文不仅仅是单纯的API函数使用介绍,从而具有相当的实用价值。
未经作者许可,请勿转载!
1 摘要
本文介绍了基于WaveX低级音频API采集音频及实时播放的技术。并对音频实时性和连续性作了比较深入的分析。利用双/多缓冲技术和网络拥塞控制策略可很好的控制音频的实时性和连续性。
2 背景介绍
WINDOWS下音频的采集,播放有三种模式:
1)通过高级音频函数、媒体控制接口MCI[1、2]设备驱动程序;
2)低级音频函数MIDI Mapper、低级音频设备驱动(WaveX API);
3)利用DirectX中的DirectSound;
使用MCI的方法极其简便,灵活性较差;使用低级音频函数的方法相对来说难一点,但是能够对音频数据进行灵活的操控;而采用DirectSound的方法,控制声音数据灵活,效果比前二者都好,但实现起来是三者中最难的。
低层音频服务及重要的数据结构低级音频服务控制着不同的音频设备,这些设备包括 WAVE,MIDI和辅助音频设备。低级音频服务包括如下内容:
(1)查询音频设备;
(2)打开和关闭设备驱动程序;
(3)分配和准备音频数据块;
(4)管理音频数据块;
(5)应用MMTIME结构;
(6)处理错误。
WaveX低级音频函数的相关声明和定义在mmsystem.h头文件和Winmm.lib库中。所以如果程序中用到这些函数,必须包含mmsystem.h这个头文件,同时导进Winmm.lib库。如下:
双/多缓冲技术可以很好的实现声音的快速连续采集和实时顺畅播放。采集声音时,缓冲满了会有一个消息,程序在响应这个消息需要几毫秒~几十毫秒甚至更多的时间,假设为Xms,如果只使用一个缓冲,程序必须在响应完该消息才再次采集声音,那么在这Xms的时间里,没有采集到任何声音;声音的播放也是一样的道理,这样声音就会不连续。因此双缓冲或多缓冲技术是必要的,让输入和输出设备可以循环使用这些缓冲,当程序在响应某块缓冲数据已满或播放完毕消息时,声卡可以继续往下一块缓冲添加数据或播放下一块缓冲的数据,如此循环保障声音的连续性。
3 相关数据结构
声音在采集(录音)和播放的时需要有一些统一的格式,包括音频格式类型,声道,采样率等信息。下面的数据结构具体描述了该格式:
其中,wFormatTag是音频格式类型,nChannels是声道数,nSamplesPerSec是采样频率,nAvgBytesPerSec是每秒钟的字节数,nBlockAlign是每个样本的字节数,wBitsPerSample是每个样本的量化位数,cbSize是附加信息的字节大小。
在打开声卡输入和输出设备之前,必须对音频的相关参数进行设置。在后面章节中将给出WAVE_FORMAT_PCM格式音频的详细参数设置。
音频数据块有一个头结构,这个结构包含了音频数据缓冲的地址,大小,已录音数据大小等信息和其他各种控制标志。这个结构适用于音频的输入(录音)和输出(播放)缓冲中。下面是该结构的详细信息:
其中,lpData是指定的缓冲块地址,dwBufferLength是指定的缓冲块大小,dwBytesRecorded是已录音数据大小,dwUser是用户数据,dwFlags是控制标志,表明缓冲的使用状态,dwLoops是音频输出时缓冲数据块循环的次数,lpNext和reserved是系统保留数据。在程序实现时,通过设置或修改这个结构的相关参数来实现对音频输入和输出缓冲区的控制。
程序中定义了一个队列结构,用来存储网络中接收到的音频数据,其结构如下:
其中,lpdata是数据块地址,dwLength是数据块大小。通过调整m_AudioDataOut下标实现队列的循环过程。
4 参数设置
声卡输入和输出的音频属性可定义如下:
设置的音频格式类型是PCM格式,单通道,8000HZ的采样率,每秒采集的数据大小为16000bytes.其中,存在着下面的关系:
音频数据块头结构可定义如下:
每次为输入或输出设备准备缓存的时候,都需要设置缓存数据块的头结构。
5 基本操作流程
调用WaveX 低级音频函数API启动声卡录音的基本操作步骤如下图所示:
图3.1 录音流程
在这个过程中,会产生很多WM_WIM_***格式的WINDOWS消息。程序通过捕获这些消息对缓存,数据和设备进行处理,具体可见后面章节。录音设备打开时,可以指定消息的响应方式:回掉函数,线程ID,WINDOWS窗口句柄或事件句柄等。
调用WaveX 低级音频函数API启动声卡输出的基本操作步骤如下图所示:
图3.2 播放流程
同录音一样,在音频输出过程中也有一系列的消息,程序通过捕获这些消息对缓存,数据和设备进行处理。具体操作见后面章节。
6 消息及处理
WINDOWS下提供消息映射实现事件的处理。低级音频函数处理声音数据块也正要归功于WINDOW的消息映射机制。
图6.1 和6.2分别描述了声卡录音和播放过程中产生的消息:
6.1 录音过程消息
6.2 播放过程消息
在打开音频输入或输出设备消息处理中,可对一些变量进行初始化;在WM_WIM_CLOSE消息的处理中主要是调用WaveInUnprepareHeader释放输入设备对应的缓存和其他资源;在WOM_CLOSE消息的处理中主要是调用WaveOutUnprepareHeader释放输出设备对应的缓存和其他资源;在WM_WIM_DATA消息处理中,处理完录入的数据后需要重新为输入设备添加缓冲;在WM_WIM_DATA消息处理中,首先清空已经播放的数据,调整循环队列中接收音频指针和播放音频指针,然后从该队列中拷贝数据到输出缓存中。
7 程序结构
系统的功能是实现在局域网内的点对点实时音频通信。音频的网络传输采用UDP方式。
程序中为输入设备准备了两块缓冲,输出设备的缓冲块数可以通过修改宏进行调整,但至少是两块。程序使用多线程技术实现声音的采集发送和接收播放。总体结构如下所示:
7.1 总体结构图
程序总体结构很简单,大体可以分为两部分:一是录音和发送,二是接收和播放。
(一) 录音发送部分的流程如下:
7.2 录音及发送流程图
部分源码:
1)打开音频输入设备源码为:
其中,hWaveIn为音频输入句柄,WAVE_MAPPER表示让系统选择录音的声卡,m_waveformin是音频格式,waveInProc是消息处理函数名,CALLBACK_FUNCTION表示以函数调用的方式处理响应录音过程中的消息。
2)为输入设备添加缓冲的源码为:
8 网络拥塞控制策略
从网络中接收到的音频数据解码后存放在循环队列中,队列中缓存块数为50,每块大小为400Bytes(网络音频数据一帧为100B,解码后为400B);声卡播放音频需要从循环队列中取数据,令接收指针在循环队列中的位置为nR,播放指针为nP,两者之间的关系如图8.1所示:
nP 和nR指向的位置一开始都是0。如果同时启动网络接收和音频播放的话就会造成同一块内存的读写冲突,导致程序崩溃,所以程序中当nR>1时才启动音频播放。
nR和nP的移动是异步的。当接收到一帧网络音频数据时nR = (nR+1)%50;而nP的移动需要视网络状态好坏而定。通过协调两者的移动步伐达到网络拥塞控制的目的。具体如下:
1)网络正常,即nR和nP的差距在2~8之间,包括2和8,则nP = (nP+1)%50;
2)网络繁忙,即nR指针移动过满,导致nR-nP < 2,则 nP保持不动,下一次播放的数据为空数据;
3)网络较好,且因某些原因声卡输出慢了,即nR-nP > 8,则 np = (nP+abs(nR-nP)%50-5)%50 ;通过丢弃一些数据帧以保证声音的实时性,否则会造成声音延时过大,而且随着时间推移会越来越大。
9 系统测试
语音通信主要测试以下几个方面:
1)实时性:话音是否有延时;
2)连续性:话音是否连续,中间是否会断续;
3)稳定性:话音是否会丢失,若会,丢失的是否严重;
程序中某些参数是固定的:音频每次采集的大小为400B,编码后为100B;接收音频循环队列块数为50。可调整的是循环队列每块缓存大小,声卡输出缓冲大小(两者的大小是一样的),以及声卡输出缓冲数目。假设缓存大小为BUFFER,缓存块数为BUFFERNUM,通过测试有以下结果:
BUFFER(Bytes) | BUFFERNUM | 话音效果 | ||
实时性 | 连续 | 稳定 | ||
400 | 2 | 好 | 不连续 | 稳定 |
400 | 4 | 好 | 有点不连续 | 稳定 |
400 | 8 | 好 | 一般 | 稳定 |
400 | 16 | 好 | 好 | 稳定 |
800 | 2 | 好 | 有点连续 | 稳定 |
800 | 4 | 好 | 一般 | 稳定 |
800 | 8 | 好 | 好 | 稳定 |
800 | 16 | 好 | 好 | 稳定 |
2000 | 2 | 有点延迟 | 好 | 稳定 |
2000 | 4 | 延迟大 | 好 | 稳定 |
2000 | 8 | 延迟较大 | 好 | 稳定 |
2000 | 16 | 延迟很大 | 好 | 稳定 |
4000 | 2 | 延迟大 | 好 | 稳定 |
4000 | 4 | 延迟较大 | 好 | 稳定 |
4000 | 8 | 延迟很大 | 好 | 稳定 |
4000 | 16 | 很差 | 好 | 稳定 |
根据之前分析及上面测试结果,不难发现在局域网中,网络比较稳定,话音基本上不丢失。
当缓冲区较小时,必须增大缓冲块数以保障话音的连续性;当缓冲较大时,必须减少缓冲块数以保证话音实时性。音频在网络拥塞控制时,如果nR移动速度比nP快,则他们之间最大差距可能是8,若每块大小为4000B,则就有4000×8=32000B数据的延迟。
所以在实际应用中,可将缓冲大小和缓冲块数设置比较合适的值,例如:400,16;或800,8;也可以调整网络拥塞控制策略优化话音效果,例如可将nR和nP的差值幅度调小作为网络正常的情况。
10 结束语
网络拥塞控制策略和双/多缓冲技术是实现实时连续话音通信的关键技术。在实际应用中建立两个UDP套接字:一个用于发送音频数据,一个用于接收音频数据,利用WaveX音频函数采集播放声音,可以很好的实现音频的实时通信
附录A WaveX API
waveInGetNumDevs | 返回系统中存在的波形输入设备的数量 |
waveInAddBuffer | 向波形输入设备添加一个输入缓冲区 |
waveInGetDevCaps | 查询指定的波形输入设备以确定其性能 |
waveInGetErrorText | 检取由指定的错误代码标识的文本说明 |
waveInGetID | 获取指定的波形输入设备的标识符 |
waveInGetPosition | 检取指定波形输入设备的当前位置 |
waveInMessage | 发送一条消息给波形输入设备的驱动器 |
waveInOpen | 为录音而打开一个波形输入设备 |
waveInPrepareHeader | 为波形输入准备一个输入缓冲区 |
waveInStart | 启动在指定的波形输入设备的输入 |
waveInReset | 停止给定的波形输入设备的输入,且将当前位置清零 |
waveInStop | 停止在指定的波形输入设备上的输入 |
waveInUnprepareHeader | 清除由waveInPrepareHeader函数实现的准备 |
WaveInClose | 关闭指定的波形输入设置 |
waveOutBreakLoop | 中断给定的波形输出设备上一个循环,并允许播放驱动取列表中的下一个块 |
waveOutClose | 关闭指定的波形输出设备 |
waveOutGetDevCaps | 查询一个指定的波形输出设备以确定其性能 |
waveOutGetErrorText | 检取由指定的错误代码标识的文本说明 |
waveOutGetID | 检取指定的波形输出设备的标识符 |
waveOutGetNumDevs | 检取系统中存在的波形输出设备的数量 |
waveOutGetPitch | 查询一个波形输出设备的当前音调设置 |
waveOutGetPlaybackRate | 查询一个波形输出设备当前播放的速度 |
waveOutGetPosition | 检取指定波形输出设备的当前播放位置 |
waveOutGetVolume | 查询指定波形输出设备的当前音量设置 |
waveOutMessage | 发送一条消息给一个波形输出设备的驱动器 |
waveOutOpen | 为播放打开一个波形输出设备 |
waveOutPause | 暂停指定波形输出设备上的播放 |
waveOutPrepareHeader | 为播放准备一个波形缓冲区 |
waveOutRestart | 重新启动一个被暂停的波形输出设备 |
waveOutSetPitch | 设置一个波形输出设备的音调 |
waveOutSetPlaybackRate | 设置指定波形输出设备的速度 |
waveOutSetVolume | 设置指定的波形输出设备的音量 |
waveOutUnprepareHeader | 清除由waveOutPrepareHeader函数实现的准备 |
waveOutWrite | 向指定的波形输出设备发送一个数据块 |
- END -
本文已获作者(林文焕)本人许可转载,其他转载请直接联系作者:linpder AT 163.com。