谈谈低延迟对音质的负面影响,顺便谈谈WASAPI

来源:互联网 发布:太原知达常青藤好进吗 编辑:程序博客网 时间:2024/04/30 14:42

By 咣輝のま裔http://blog.sina.com.cn/s/blog_637d7cd80101pmwg.html
转载请注明作者信息,谢谢。

常常看到某些人给别人推荐ASIO,KS或者WASAPI的时候,总喜欢扯上一句:拥有更低的延迟,从而改善音质。
事实到底是不是这样的呢?让我们来看看Foobar2000作者的看法。
Note that low latency playback is relevant to real-time processing and editing only. It’s completely useless for music playback; in fact, higher latency is better in this case as it gives better protection against glitching from buffer underruns.
需要注意的是低延迟播放仅与实时处理和编辑相关。对于音乐播放是完全无用的。事实上,更高的延迟在这种情况下表现得更好。因为它提供了更好的保护,防止因为缓存欠载造成的毛刺。

作者这一两句话完全概括了重点,但是并不是所有人都能真正理解其中的道理。
低延迟的唯一好处:
低延迟的唯一好处仅在于录音播音的实时处理,比如将一个人说的话通过扩音器实时的广播,将播放的背景音乐与录制的人声进行实时混音处理等等对时间要求很高的操作。事实上低延迟是为了达到实时处理而不得不做的一种妥协。
什么是缓存欠载:
缓存欠载是指由于某种原因导致系统传输停顿使缓存不能及时补充有效数据,同时缓存中的数据又已被播放(录制)完,造成缓存中数据为空的现象。
为什么低延迟会造成缓存欠载:
实际上 延迟 = 采样点个数/采样率 + 偏移量 而多数情况下偏移量大致为0,于是多数情况下延迟 = 采样点个数/采样率 采样点个数反映了缓冲大小,缓冲大小决定了当前写入位置与播放位置的距离。
播放速度是固定的,如果缓冲大小为0,那么就必需时刻保持写入速度等于播放速度,这势必会造成系统频繁调用的高负载。当我们加入了缓冲机制,系统对于写入速度的要求就从瞬时速度降低为平均速度的水平。缓冲越大,对于突发高负载造成的写入速度降低的缓冲能力就越强。
(在播放的时候偏移量可以为负数,这样就实现了0延迟,让写入与播放同时进行,由于写入速度快于播放速度,而且播放的内容是可以预知的,这样就能够在预存了足够的采样点后,再保持速度同步。事实上不管是Foobar2000的WASAPI,还是ASIO,设置的都是缓冲大小。)
缓存欠载的后果:
根据处理方式的不同,缓存欠载会有两种后果:1. 禁音,短时间的禁音在人耳听来就是爆音的效果。2. 重复播放缓冲区,直到有新数据为止,这种处理方式在人耳听来就是一段很短的声音无限重播。

接下来要说些比较专业的话题了,如果你看不懂也没关系,大致看下,知道楼主的结论就好了。
让我们看看软件播放过程中的实际情况:
事实上,写入声卡缓冲的速度远快于播放的速度。为了不让线程在写完一个缓冲大小的数据后空载,增加不必要的消耗,有违节能环保的精神。于是每次在写入操作完成后进入睡眠状态(调用Sleep或者timeSetEvent+WaitForXXXX函数)。
菜鸟级程序员的做法:
这里写图片描述

菜鸟级程序员每次都让播放与写入同时开始,写完一个缓冲大小的数据后,便睡眠一个延迟的时间,等待播放完毕。然后又开始播放和写入,不停的重复下去。
这中做法并没有发挥出缓冲的所有优势,只能保证一个缓存大小中的声音是没有差别的。但是问题发生在两个缓冲的交汇处。睡眠并不能保证在精确的时刻被唤醒。每块缓冲大小的声音在时间上总是或多或少的前后漂移。一段1S的声音在100ms的延迟下,将有9处交汇处,在10ms的延迟下有99处交汇处,在1ms的延迟下有999次的交汇处。将延迟从100ms降低为1ms,对声音的影响加剧了100倍还不止,而系统频繁调用的次数也增加了100倍不止。
以下测试都在没有干扰的情况下进行:
20ms延迟的情况:
这里写图片描述

平均时间漂移(正负不抵消)体现了两个缓冲交汇处对音质的影响,累计时间漂移(正负不抵消)表示了这种处理方式下对于音质的总体影响,可以看到30S的声音就有381ms出现了问题。实际上,如果是日常的使用中,最大时间漂移可以轻易突破20ms。
200ms延迟下的对比:
这里写图片描述

可以看到加大延迟对于菜鸟写的播放程序有极大的好处,累计时间漂移(正负不抵消)直接从381ms降低到了20.9ms
配合双缓冲,循环播放,动态写缓存:

这里写图片描述

这里写图片描述

这里写图片描述

这种做法完全发挥出了缓冲的价值。只要最大时间漂移和累计时间漂移(正负抵消)不超过一个延迟的时间,就不会对音质产生影响。此时在前两项达标的情况下,平均时间漂移(正负不抵消)和累计时间漂移(正负不抵消)已经不会对音质产生影响。延迟越小,对于突发的高负载,抵抗能力就越差。
那么问题到这里就已经顺利解决了么?
我们知道不管Sleep也好还是timeSetEvent + WaitForXXXX,其时间精度都受到系统定时器分辨率的影响。系统默认情况下是15.6ms,Foobar2000 以及一些调用MMCSS (Multimedia Class Scheduler Service)的程序都会将定时器精度设置为10ms。然而这个值并不是你想设为多少就能设为多少的,这个值只能等于系统中所有程序设置的最小值,而且最高精度只能为1ms,这个值设的太小还会增加系统线程切换的消耗。
那么如果音频的延迟为25ms,在10ms的时间精度下,Sleep会如何表现呢:
这里写图片描述

结果就是每次都睡眠30ms。最终时间严重增长了5S。为了解决这个问题:
timeSetEvent + WaitForXXXX登场:
这里写图片描述

可以看到,在10ms的精度前提下,等待时间不断的在20ms和30ms间变化,实际上延迟25ms的表现还不如20ms,其对于突发高负载的承受力只有15ms的水平。在延迟足够大的情况下,这种方式足以解决简单Sleep带来的问题。
遗憾的是,系统定时器分辨率的精度最高只能是1ms,这就给不是整数毫秒的延迟带来了问题。
事实上,44100Hz在多数采样点下的延迟并不是整数毫秒。
WASAPI Event-Mode 对于采样点的限制如下:
最高500ms
大小不超过512KB
必需对齐到128Byte(是Byte不是bit)
拿20ms来说,实际上的延迟时间是20.3175ms 采样点896个,这多余的0.3175ms对于timeSetEvent + WaitForXXXX造成问题:
这里写图片描述

累计时间漂移(正负抵消)已经超过了延迟的20倍。这就是时间零头积累造成的问题。为了解决这个问题:
优化的Sleep – 最后的绝唱:
现在我们不再睡眠一个延迟值了,现在:
睡眠时间 = 当前循环次数n*延迟 - (当前系统时间 - 播放开始的系统时间)
这样便解决了零头时间带来的问题。
这里写图片描述

可以看到平均时间周期已经和延迟基本接近了,累计时间漂移(正负抵消)也得到了很好的控制,但是最大时间漂移超过了20ms,这不是偶然,实际上楼主测试了5次,其中4次都超过了20ms,1次为6ms。
在系统时间精度为10ms的前提下,想要在这种方式下良好工作,延迟至少要30ms。实际上这是Push-Mode下不得不采用的方式,只有这种方式才能解决上面提到的所有问题。Foobar2000便是这么做的。这也是其缓冲长度必需大于50ms的原因。虽说Foobar2000的缓冲长度与声卡的缓冲大小不是同一个事物,但两者前后承接,工作原理上极其相似。
(其实系统计数器的分辨率可以达到100纳秒的级别,但是用在播放器上完全是杀鸡用牛刀。100纳秒级别的计数器在大型游戏上常常用到)

WASAPI Event-mode来袭
只有降低两个缓冲区交接处的时间误差和漂移幅度,才能在音质不受损的情况下,降低延迟。
我们知道在当前系统时间精度下,为了保证音质不受损,延迟不可能降得太低。为了降低延迟,我们迫切的需要一种不需要高负载轮询,又能高度准确的时间。硬件驱动回调模式便由此孕育而生。不论是WASAPI Event-mode 还是KS又或者是ASIO,采用的都是这种方式。这才是他们的精髓所在。
声卡在每次播放完一个缓冲区后便发送事件通知程序,应用程序便开始将音频数据写入缓冲区。详细图解,请回头看上面的:配合双缓冲,循环播放,动态写缓存。
硬件驱动回调模式的唯一区别就是:现在不是由我们自己定时写入了,而是让硬件告诉我们什么时候该写入了。这便使得我们写入的时间相当准确,从而对于缓冲大小的要求降低,最后延迟就可以控制的更小。
现在楼主写的基于WASAPI Event-mode的测试播放器该登场了:
这里写图片描述

将你要播放的文件命名为a.wav放在同一目录下,然后运行程序就可以播放了。
只支持wav格式,只支持声卡硬件支持的格式,支持声卡选择。
文件采用全文件缓冲的方式载入,请确保拥有足够的空闲内存(事实上,楼主并不认为全文件缓冲会改善音质。只要声卡缓冲大小(延迟)足够大,全文件缓冲就没有任何意义。楼主这么做完全属于偷懒的做法)。
实际上WASAPI Event-mode的兼容性可能在不同的声卡下表现有所不同,楼主基于自己的声卡修正了24位音频文件的播放问题。如果在你的机器上播放24位音频格式时出现杂音,请留言反馈,楼主可以再发一个未修正的版本。
想要查看声卡支持的格式请使用楼主写的声卡参数测试软件:
这里写图片描述

还能顺便检查下最小延迟以及系统定时器分辨率。
以上两个软件基于WASAPI,不支持Windows XP系统。如果遇到问题,可以留言反馈。
如果Windows Vista以上系统无法运行,请安装Visual C++ 2010运行时组件
http://www.microsoft.com/zh-cn/download/details.aspx?id=8328
http://www.microsoft.com/zh-cn/download/details.aspx?id=13523
那么我们开始测试:
这里写图片描述

可以看到累计时间漂移(正负不抵消)减小到了原来的一半,虽然这没什么作用,但是也可以作为一个浮动的观察值。最重要的是,最大时间漂移降低到了2ms以下。可以说在系统单纯播放音乐,没有干别的事情时,5ms左右的延迟就已经完全足够了。
你还观察到累计时间漂移(正负抵消)为负数,实际上这是因为声卡的时钟与系统的时钟不同造成的。就好像两杆秤的1Kg不一样。如果以声卡的时钟为标准来衡量的话,误差应该很小,只是微妙级别。我前面已经说过了,只要累计时间漂移(正负抵消)的值小于延迟,就不会对声音造成任何影响。

空闲时的测试可以说是太轻松了,让我们做下压力测试吧:
在已运行极品飞车17的情况下,对一首6分30秒192000Hz 24bit的歌曲进行播放测试。
以下测试都做了3次以上,不存在突发情况造成的误差。
延迟2.25ms下的结果
这里写图片描述

点评:歌曲播放时间直接增加了11秒,最大时间漂移达到40.2ms,是延迟的18倍。这么低的延迟,不但严重影响了音质,还造成了线程频繁切换调用的高负载。楼主已无法忍受,直接丢掉了耳机,默默地开着车。
延迟20ms下的结果
这里写图片描述

点评:累计时间漂移(正负抵消)和最大时间漂移都超过了延迟,虽然不多,但还是对音质产生了一定的影响。显然至少要有50ms的延迟才能完全满足实际情况。
延迟200ms下的结果
这里写图片描述

点评:最大时间漂移降低到了1.6ms,由此可以看出对系统和声卡的压力降低了许多,更加稳定了。明显舒缓了许多,楼主开车头都不晕了,心情也不急躁了,当然这很有可能是心理安慰的作用,哈哈。
最大值:341.25ms下的结果
这里写图片描述

点评:与200ms相似的结果,完全在误差范围之内。进一步减轻了系统的负载,对突发事件也拥有了更强的抵抗能力。
楼主写的最后一个软件,也是前面一直用到的测试工具该登场了:
这里写图片描述

   多数选项对于你的来说意义不大,不过我还是要推荐下第6项(这个工具XP也能运行)

这里写图片描述

事实上我们无法明确知道Foobar2000等软件采用的缓冲写入算法究竟是否给力,但是我们可以通过计算,得出几个兼容性最好的延迟时间。
上面列出的便是44100Hz 16bit下最具兼容性的延迟。强烈推荐320ms,这是44100这个采样率能够获得的最小整数时间。
如果你希望一个较小的延迟的话,可以设置为50ms:
这里写图片描述

上面提到的3个工具的下载地址:
声卡检测工具+播放器+延迟测试工具
http://pan.baidu.com/share/link?shareid=3260973561&uk=1494056105
关于Foobar2000的设置建议:
这里写图片描述

其实正如Foobar2000作者所说的,只要缓冲足够大,便不会对音质产生多大影响。如果你追求完美的话就把缓冲长度设为320的整数倍,比如1280吧,理由我前面都说过了。
如果你喜欢WASAPI的话就在设备里选择上,然后在高级里这么设置:
这里写图片描述

本来Event-Mode的缓冲也应该设为320的。很可惜的是,作者写的event mode插件存在很严重的bug,导致延迟大于90ms左右便会声音播放错乱,严重缩短播放时间,这极有可能是WaitForXXXX函数超时时间设置过小造成的。所以我不推荐你用Event-Mode,我更倾向于使用320ms延迟的push mode(实际上push mode最大的缓冲时间能达到2000ms,作者限制为1000ms,估计是在内部做了双缓冲处理)。
线程优先级勾上MMCSS,模式输入Pro Audio,Pro Audio属于多媒体调度优先级最高的模式了。
全文件缓冲这个除了浪费内存,没有任何好处,按作者默认设置0就好:)

终于看完了,如果你觉得本文写的不错,看了之后有所收获,欢迎转载。

0 0