最近在制作IP话务坐席客户端,在这个系统里,需要用声卡去播放从服务器传来的音频数据,因为电话通讯是实时的,所以不可能等到音频数据都传完了再播放(废话),所以这个播放过程应该是近似于流媒体的方式,有多少数据就播放多少数据(还是废话)。
好吧,废话少说,切入正题。
由于上述原因,我只能选择用低级波形API去播放音频数据,即使用Multi-Media Library。这是WINDOWS下最接近底层的音频API,当然,我们还有一个选择DirectSound,不过那个用起来没有Multi-Media Library那么方便,而且我也不需要用到那么高的特性。
在开始之前,我们先来了解一下波形数据的格式和特性,有这么几个概念需要先熟悉:“声道”、“采样率”、“样本位率”。
“声道”的意思很容易理解,我们通常听说的“单声道”、“双声道”、“环绕立体声”就是声道的概念,简单点说,就是有多少个音源(抱歉,我不知道这个解释是不是十分精确,因为我并不是搞音频工程学的)。 “采样率”的意思是每秒钟采集多少个声音样本,越多越清晰,而采集到的数据也就越多;反之,越小越模糊,采集到的数据也就越少。采样率的单位是hz(赫兹),8000hz就代表每秒采集8000个样本,更高的采样可以得到更清晰的声音,但是对采样设备的能力和网络的速度也就要求更高些。“样本位率”的意思是每个样本占多少位的数据量,一般有8位、16位、32位(浮点)这三个选择,位率高则采样数据精确,位率低则采样数据有失真。
有了上述概念,我们就可以计算出波形数据的数据量了,假如说有一段波形音频数据,是双声道,44100hz的采样率,16位的样本位率(一般CD中都是这样的格式),那么,这个音频数据每秒的数据量就是
(2 * 44100 * 16) / 8 = 176400字节
也就是说,计算公式是:
(声道数 * 采样率 * 样本位率) / 8 = 每秒字节数
为什么除8?一个字节占8位嘛
为了描述上面的内容,Multi-Media Library定义了一个struct
wFormatTag指的是格式类别,其值在MMREG.H头文件中定义,下面是部分格式的摘录:
在本文中,我使用WAVE_FORMAT_ALAW格式,因为我使用的程控交换机输出的就是这种格式。
播放音频数据的API有如下几个,并不多,也很简单。
waveOutOpen – 打开波形输出设备
waveOutPrepareHeader – 准备播放缓冲区
waveOutUnprepareHeader – 取消播放缓冲区
waveOutWrite – 将数据写入波形输出设备
waveOutReset – 波形输出设备复位(清除正在播放的数据,停止播放)
waveOutPause – 波形输出设备暂停(暂停播放)
waveOutRestart – 波形输出设备恢复(继续播放)
waveOutClose – 关闭波形输出设备
处理顺序大致上就是:
waveOutOpen -> waveOutPrepareHeader -> waveOutWrite -> waveOutUnprepareHeader -> waveOutClose
不过有个问题,你几乎不可能一次性就将所有要播放的数据全部写入,流模式数据的播放就更不可能,因此,必须将要播放的波形数据分批分次的写入设备。不过这又带来另一个问题,如果分批次的写入,在第一个数据播放完后接着写入下一个数据的话,无论你的计算机有快,都会有暂时的停顿,那么听起来,声音就一卡一卡的。
这个问题当然可以解决,否则便不会有此文了,相信所有播放器都是用类似的方式解决的。waveOutWrite函数有个特点,即音频数据写完后函数会立即返回,并不等待声音播放完毕,而且如果此时立即再写入另一个数据,那么当第一个数据播放完后,系统会自动播放第二个数据,中间不会有停顿。所以,我们可以建立一个双缓冲(或者多缓冲也可以),一次性写入两段数据,当第一段缓冲区数据播放完毕时立即用第三段据去填充它,此时第二缓冲区数据正在播放,所以不会停顿,当第二段数据播放完毕后第三段数据已经就绪,所以也不会停顿,此时再用第四段数据去填充第二缓冲区,第三段数据播放完毕后再用第五段数据去填充第一缓冲区……
流程如下:
那么,如何得知某一段数据播放完毕了呢?别急,先来看看waveOutOpen的原形
这个函数用来打开波形输出设备,如果成功,将返回MMSYSTEM_NOERROR,否则返回错误代码。
phwo是返回的设备句柄,如果函数返回成功,这个参数将会返回打开的设备句柄,后面的操作都需要用到这个设备句柄。
uDeviceID 是要打开的设备ID,因为系统中可以拥有多个波形输出设备,用此参数来指定要打开哪一个设备,如果要打开默认的波形输出设备,指定为WAVE_MAPPER即可。
pwfx 就是前面介绍的WAVEFORMATEX结构体,指定要在这个设备上播放什么格式的波形数据。
dwCallback 指定设备的回调,可以是回调函数的指针,也可以是事件句柄,也可以是窗口的句柄,或者线程ID。
dwCallbackInstance 指定回调时的用户数据,可以指定任意数据,数据将在回调产生时作为参数传入(窗口回调的情况下此数据不可用)
fdwOpen 打开设备用的标志,具体有哪些值可用请参考MSDN,我这里只用CALLBACK_FUNCTION,表示用回调函数的方式执行回调。
再来看看waveOutPrepareHeader函数的原形
这个函数用来指定设备的播放缓冲,在播放波形数据前,必须先使用这个函数来指定播放缓冲。
hwo仍然是设备的句柄
pwh是播放缓冲的结构体指针,下面将详细介绍它
cbwh是上面缓冲结构体的字节数,用sizeof计算即可
pwh是WAVEHDR结构体的指针,WAVEHDR的原形是:
lpData是要播放的数据块的指针
dwBufferLength是要播放的数据块的字节数
dwBytesRecorded是已录音的字节数(仅在录音时用)
dwUser我们可以在此指定任意数据
dwFlags是系统指定的状态值,在调用waveOutPrepareHeader前,必须将它置0
dwLoops是循环播放的次数,这里我用不着,置0即可
lpNext和reserved都是备用字段,置NULL
下面要介绍waveOutWrite函数,原形如下:
这个函数用来将播放缓冲中的数据发送到波形输出设备,其参数和waveOutPrepareHeader是一样的,需要注意的是:lpData,它指定的指针位置在调用waveOutPrepareHeader后不可以再变化,但是我们仍然可以改变指针所指位置的数据;dwBufferLength的值可以改变,但是必须比调用waveOutPrepareHeader时指定的值小,也就是说,可以播放比指定的缓冲小的数据。
剩下的几个函数由于都很简单或者和上面的函数类似,我这里就不再浪费口舌了。
如果设备的状态发生变化,如设备已打开、设备播放完毕,设备已关闭,系统就会执行回调,我这里只介绍函数回调的情形,其回调函数的原形如下:
这里有几个参数是很重要的,nMsg告诉你现在发生了什么事情,WOM_OPEN表示设备已打开,WOM_DONE表示设备刚播放完一块缓冲,WOM_CLOSE表示设备已被关闭;dwInstance是你在打开设备时指定的dwCallbackInstance值;Param1仅在nMsg的值为WOM_DONE时有效,指示当前播放完的是哪一块缓冲。
如此一来,我们就可以得知哪一块缓冲播放完毕,并立即就可以准备好后续缓冲块。
在程序中,我使用一个波形缓冲来保存接收到的数据,开启4个播放缓冲。为了让波形缓冲中的数据及时进入播放缓冲,我开启了一个线程,只要波形缓冲中有足够的数据可以播放且播放缓冲没有用完,就往里填充数据,播放完一个播放缓冲后,就立即继续填充它以保证流畅的播放效果,在这里,我使用了事件对象来判断缓冲是否已播放完毕。
下面给出播放类的源码(Borland C++ Builder):
Player.H
2
3
4
5
6
7
8
9
10
11
12
13
Player.CPP
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
源码已经更新,请到此处下载
嘿嘿,第一次写那么长篇大论的东西,如有遗漏或者错误,各位包涵则个:)
参考资料:
http://www.codeproject.com/audio/wavefiles.asp
(他的代码有点BUG,而且结构不是很易懂,所以我作了较大改动)