whjfc
发表于 2005-9-16 19:05:00
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有帐号?快速注册
x
视频会议以其方便、快捷、“面对面”交流的优点逐渐得到了人们的认可,许多企事业单位、教育单位,医疗单位都希望使用视频会议来代替传统的会议形式。在视频会议中,与会者之间主要传输的是音频数据和视频数据,其中的音频数据显得更为重要。因为会议中的大部分有用信息都包含在与会者的言语交流上,所以视频会议系统必须保证音频通信的流畅性和全双工,才能使视频会议更接近于真实的会议环境。 ' I# [/ L! m- |7 z7 y/ F! i
0 D1 `8 r# h) G$ ?: d9 G+ G* C* U DirectX是Microsoft开发的专门用于开发游戏和多媒体软件的应用程序接口(API),包括了对二维和三维图像、声音、音乐和针对网络多人游戏的网络通信的强大支持。DirectX是一种标准的软件接口,所有主要的硬件供应商都提供支持DirectX的驱动设备,应用DirectX的软件可以在不同的硬件环境下正常运行。另一方面,DirectX能根据所使用的不同硬件,来选择适当的方式使用硬件加速能力,便于开发高质量的多媒体和游戏软件。在DirectX所提供的众多组件中,用于音频处理的是DirectSound组件。为保证视频会议系统中语音的流畅性,需要采用DirectSound中提供的StreamingBuffer(流式缓冲)机制来实现。而为了保证视频会议系统中的全双工音频通信,主要利用的则是DirectSound中的混音机制来实现。
0 U7 m9 }" h6 A- |2 r7 @1 `$ n: B; s4 X2 r+ M
利用StreamingBuffer实现流畅的语音交流
( d+ @9 u. w0 t& R
8 _6 t! k3 D( W/ c* c! ` DirectSound中提供了两种缓冲机制,分别是StaticBuffer(静态缓冲)和StreamingBuffer(流式缓冲)。StaticBuffer指一次将一段完整的声音存入缓冲中;StreamingBuffer指的是并不将全部的数据一次读入缓冲,而是在播放声音时动态地读入,占用空间较小。一般来说,如果声音需要反复播放而且容量有限(如游戏音效),使用StaticBuffer更有助于提高程序的效率;相反,如果是容量很大、实时性要求较高的音频数据流,则使用StreamingBuffer为佳。在视频会议系统中,如使用StaticBuffer,则在向缓冲区写入新的音频数据时,声音的回放必然出现短暂停顿,使与会者的完整话语不能够连续播放,影响通话的流畅性,而StreamingBuffer可克服语音不连续的缺点。
1 J; e+ |/ z8 q+ V1 z* }% J4 h; {) S* a* z! C2 Q) E
StreamingBuffer提供了两个指针:PlayCursor(回放游标)和WriteCursor(写入游标),它们的值只是相对于缓冲区开头的偏移量而非绝对的内存地址。其中PlayCursor总是指向下一个被输出的数据字节,而WriteCursor指向的地址则指明从哪个地方开始可以安全地写入新的音频数据而不影响回放。按回放音频数据的顺序来看,WriteCursor总是在PlayCursor之前,并且它们间保持着一定的间距,而这个间距会根据不同的系统状况而有所不同,实验表明这个间距大概是100~200字节左右。当开始对缓冲区中的音频数据进行循环模式回放时,总是在PlayCursor所指的地方开始。回放后PlayCursor和WriteCursor会保持它们的间距等速度前移,并且PlayCursor总是指向下一个被输出的数据字节。当回放到达缓冲区的结尾处时,PlayCursor将重新指向缓冲区的开头,如此循环下去。而当程序停止对StreamingBuffer中的音频数据进行回放时,PlayCursor则不再移动,并停留在下一个被输出的数据字节处,直到重新回放才会继续前移。
& y+ T; q5 z6 P4 a1 e0 I2 U6 ?5 u
# f8 Q1 @/ x/ E; n- L3 a另外,在PlayCursor和WriteCursor之间的区域被认为是即将要进行回放的数据,所以不能够对其做更新。在理解了StreamingBuffer的基本工作方式后,接下来详细阐述如何用VisualC++作具体实现,其中会涉及到一些VisualC++的函数,具体可参考MicrosoftMSDN。 " B2 d- C3 l! c j
S: z& A/ m( q( Y; }3 u& q
2 K) c% W9 t6 b2 h: ^3 S
, c4 Q# I) S# L2 s( A 在程序中,设置一个大小为一帧音频数据的大小(一般相当于0.25秒的语音)的2倍的StreamingBuffer。并且在StreamingBuffer的正中间和结尾处分别设置标志一个触发事件。程序开始时,通过调用Play函数对StreamingBuffer中的数据进行循环回放。当PlayCursor到达正中间和结尾时,事件就会产生,就可以通过程序向缓冲区写入新一帧的音频数据。在写入新一帧音频数据的过程中,首先调用Lock函数锁定缓冲区中的部分,此时的WriteCursor被锁定不再前移,而PlayCursor将跟随着声音的回放继续前进;利用回放PlayCursor和WriteCursor间的音频数据的一段时间内,根据锁定时获得的lplpvAudioPtr1(此时的lplpvAudioPtr1指向的地方就是锁定时WriteCursor的所指的地方),lpdwAudioBytes1(可安全写入的音频数据大小)等与StreamingBuffer相关的参数lplpvAudioPtr2、lpdwAudioBytes2等信息,把数据在指定的地方写入缓冲区,然后调用Unlock函数解除对WriteCursor的锁定。这样,WriteCursor重新调整回与PlayCursor保持100~200字节间距的地方,继续对新的音频数据进行回放。上述这个过程在整个程序的运行过程中,不断地循环进行,如图1所示,实现了在对StreamingBuffer中旧一帧音频数据进行回放的同时写入新一帧的音频数据。
: B8 r; k$ q5 i7 g$ S9 m9 ~$ W( u
从理论上讲,这已经保证了音频回放的流畅性。但在实现过程中,由于操作的对象是一帧的音频数据,其回放的时间仅是0.25秒,所以必须考虑的一个问题是程序的反应速度问题。如果忽略由事件触发到真正用Lock函数锁定缓冲区的部分以进行新数据写入之间的时间,则这种实现方法没有任何问题。除了最开始的两帧数据外,新的一帧数据会紧跟在前一帧数据之后,彼此之间没有重叠部分,也没有空隙存在,能很好地达到音频回放的流畅效果。但事实上,由事件触发到真正用Lock函数锁定缓冲区的部分以进行新数据的写入之间还必须经过线程监听到事件,分析事件对应的缓冲区,然后再触发相应的回调函数来进行) c+ z. M; }; F6 Z; c' V
2 a+ }# k, ~, |; e 上面所述的新一帧音频数据的写入过程。而这一系列分析工作所占用的时间是会随系统当时的状况而变化的,是一个随机的时间长度,所以每次对缓冲区用Lock函数锁定缓冲区的部分时,WriteCursor所在的位置都会不同,这样就造成新的一帧数据并不一定会严格地紧跟在前一帧数据之后,它们之间可能会出现重叠部分,也可能会有空隙出现,不利于音频数据的连续播放。如果出现重叠部分,那么回放造成有部分的音频数据丢失;如果有空隙的出现,会造成语音的不连续或混乱。但经过调试,仔细分析了由事件触发到真正用Lock函数锁定缓冲区的部分以进行新数据写入之间的时间后,发现它对锁定时WriteCursor所在位置的偏差产生的波动不大,一般由此产生的重叠部分或空隙部分都在50字节左右,也就是说平均每帧数据中会有50字节的错误。在程序中,指定的一帧音频数据为2000字节(与0.25秒相对应),所以会有大概2.5%的音频数据会出错。如果以所采用的音频格式来计算,8KSPS(采样率)*8Bit(每个采样用8位表示)=64KBit/s=8KB/s,那么这2.5%的错误在每秒钟内对应的会是0.025s的音频数据,基本上人的听力是难以分辨的。所以在采用StreamingBuffer依然能很好地达到了音频的流畅性要求。 |
|
|
|
|