——副标题:给音频软件硬件工程师的提示
原文:Programming Volume Controls - dr-lex.be
因为很多程序员缺乏对人类听觉系统的常识,或者只是因为太懒了,很多音频软件都存在一个很烦的问题。当你使用这些音频软件的音量控制的时候,会觉得痛不欲生。如果你有机会参与到音频软件的开发中去,那么请仔细阅读下面这些文本,把它提到的知识烧进自己的脑子里去,甚至跟身边的人奔走相告!
太长不看
- 音量推子是非线性的
线性的音量控制器真的是烦死人了!因为人类对响度的感知是对数的!这也是为什么所有的音频设备都用dB来标示它的音量控制或者增益控制。
对于一个相对的振幅
将振幅乘以特定的数值,那么对应的分贝值则增加了一定的数值。
此外,dB(A)经常被用来衡量人类感知到的绝对响度。人类平均可以感知到的最小的声响,被标记为0dB(A),一个“安静”的房间大概会有±30dB(A)的声响。
- 一个音量控制器不应该是基于百分比的
因为百分比即是线性的。
但是如果这个百分比表示的是dB值的百分比,那就还可以接收。比如0%表示-60dB而100%表示0dB。
- 一个理想的推子应该符合指数曲线
这条曲线的公式为
对于这条曲线来说,它的最小值代表“最安静”(对消费级产品来说是30dB(A)),而其最大值对应了该音频产品的最大响度。
有个问题是,你在很多情况下只能去猜测用户使用的是什么产品。所以,除非你是在做一个有明确的标准的高端产品,其实你是可以做一些猜测或者近似的。
一个很好用的估计值是用户拥有60dB的可调整范围。
- 表1给出了一些参数a和b的经验值
表1中给出了在不同响度范围下,参数
其中,x是滑块的位置,其取值范围为
大概率上,你会使用的是60dB范围上的参数。
如果你想要让推子推到最小的时候,得到完全不输出信号,那么你可以在接近0的地方给函数设置一个线性的滚降(roll-off)。
- 不连续的控制器
如果你的音量控制器是离散的,比如说,用鼠标点击按钮、或者鼠标滚轮来控制,那么最好确保每两“格”音量之间的区别在1~3dB之间。
小于1dB的音量变化难以被察觉到,而3dB以上的变化则太过于粗糙了。2dB是一个蛮不错的选择。
- 近似曲线
如果出于某些原因,你不想做指数运算,那你可以用一条所需的运算量低得多的幂次曲线来代替。
对于典型的60dB范围,这是一条4次曲线。换句话说,让波形振幅乘以系数
表1中也给出了不同动态范围下适用的近似曲线。虽然这些曲线不如对数曲线那么完美,但是再怎么说都好过你直接塞个线性的音量推子进去!
本文比较长,正文的前半部分都在讲关于分贝的基础知识。如果你只想看关于音量推子的部分,可以跳过前半部分,从“寻找理想的曲线”一节开始阅读。
为什么万恶的线性的音量控制器是邪恶的
当今的大部分音频软件都会带有控制音量的推子或者旋钮,其目的是模仿传统的音频硬件。不幸的是,大部分的这些软件控制器上都会有一点让人屁眼疼。那就是它们是线!性!的!
你也许会问:一个线性的推子犯了什么错?一端是0,一端是100,中间呈线性增长,不是很棒吗?答案是不不不不不。
你可以这样子试试看:打开一个音频软件,播一首你喜欢的曲子。把音量旋钮打到最大,然后稍稍搓动它。接着,把音量调到最小,然后稍稍搓动它。
如果这时候你听到,在音量最大的那一端基本没什么音量变化,而在最小的那一端疯狂变化。那么这个控制器十有八九就是线性的控制器了![1]
(如果你身边的播放器都做了正确的事情,你可以看看译者补充的这个视频感受下区别。)
这种邪恶甚至已经渗透到一些硬件上了!Velleman卖过一款可以拆卸组装的图形均衡器K4302,在我1995年买到的版本上,搭载的推子就是线性的,也不知道现在修正了没有。
甚至iMac G3上的音量控制器也是线性的。
恐怕这些只是众多使用了线性推子的硬件中的一角。
除了上面提到的,线性的音量控制器还会导致这些症状:
- 如果你从最小的音量网上推,只要推一点点就已经太大声了
- 从差不多中间开始,到最大音量,几乎没什么变化
- 如果一个高灵敏度的耳机和低输出的音响接到同一台电脑上,基本不可能对耳机进行微调。因为要拖动很大很大的距离,才能让音响出现音量的改变
种种这些问题,就会让用户觉得你的产品难用,但是又找不出为什么,最后只能烦到大骂:
这些音量推子……简直就像一个个秤砣!
问题出在哪里?
所以线性的音量推子到底怎么了?其实是因为我们对声音的感知是对数的。
也就是说,相同的振幅变化,在振幅本身就小的时候,我们感知到的变化程度要比振幅本身比较大的时候更大。
这种特性让我们能既能听到振幅很小的声音,也能听到振幅超大的声音。或者说,我们的听觉覆盖了很大的动态范围。
而放到音量控制上,线性的推子给出的音量变化,在我们听来是呈对数变化的,所以会觉得怪怪的。上图就是一条对数曲线。横轴上标记了两个完全一样长度的区域(也即线性的音量推子)。而纵轴则显示了听觉上的音量变化。在音量较小的那段,变化的程度要比较大的那端大。
为了得到一个“真正的”音量推子,我们只需要让推子的输出是指数的。因为
在下文中,我假设音量推子和整个音频系统,都工作在
寻找理想的曲线
指数曲线有两个很烦的属性:
- 一是只有自变量是负无穷时,它的值才是0
不过这个问题不大,因为我们的耳朵并没有拥有无限的敏感度。我们只需要知道一些实用的动态范围就行了。这部分在下面再展开讲。
- 二是指数函数的一般形式
的图像,即使确定了两个点之后,还是无法定下具体的三个参数。
所以在音量控制的场景中,我们省去
我们要得到的曲线,一定会经过
剩下的问题就是,确定控制曲线形状的
你也许会尝试把第二个点选择为
一般的环境中,都存在一定的背景噪声。如果一个声音的响度低于这个背景噪声的响度,就可以是不可闻的了。最大的问题是,即使不同人的听阈大致上是一致的,但是不同的音响系统在相同的输入信号下,输出的响度却和大量的参数相关。
为了得到合适的
所以我们就需要做一些假设。这里插一个题外话,就是“响度”是如何被测量的。
声强的测量
因为人类的听系统具有对数的响应曲线,人类定义了一个特殊的单位,以Graham Bell的名字命名的“贝尔(Bel)”,来衡量声音的大小。但是,贝尔的跨度太大了,所以我们经常接触到的单位,是贝尔缩小十倍得到的“分贝”,用dB来表示。1贝尔=10分贝。使用dB作为单位时,可以以绝对标度或者相对标度来计算。
当使用绝对标度时,我们得到的是测量的声音对于平均人群来说有多大声,用声压级(Sound Pressure Level, SPL)表示。这个标度有几种不同的变体,最常见的是dB(A)。
要使用dB(A)来度量一个声音,首先这个声音要通过一个关于平均人群的频率响应曲线的滤波器。接着,经过一个以10为低的对数变化,最终再乘以10. 这里不会讲到再深入的细节去了,因为这些在这篇文章里已经够用了。
你应该知道的一点是,0dB(A)代表了平均人群能感知到的最小响度,也即听阈。而在一个安静的环境里,通常来说都会有大约30dB(A)的背景噪声。处在一个0dB(A)的环境中,其实反而是一种很奇怪的体验。
人类所能听到的最大响度,大概是120dB(A)。一支传统的管弦乐团演奏音乐时的响度大约时94dB(A)。
需要注意的是,由于dB的对数运算,将一个声音的功率放大10倍,其实相当于增加了10dB(A).
在几乎所有的物理度量中,都会使用到相对标度。相对标度可以表达两个不同的信号或物理量之间的差距。
相对标度的符号就是简单的dB。其计算方法与你要计算的物理量是振幅还是功率有关。
对于功率值来说,其公式为
对于振幅来说,其公式为
产生这种区别的原因是,功率正比于振幅的平方。在对数运算中,平方可以提取到对数运算符的外面,变成系数2.
理论上来说,绝对标度和相对标度不能真正地互换。当我们对一个90dB(A)的声音衰减20dB,我们其实无法保证得到的声音就是精确的70dB。(译者不太理解这一点)
寻找理想的曲线(第二部分)
在我们学习了这么多关于分贝的度量的知识之后,我们可以继续刚刚寻找两个参数的问题了。我们要确保这条曲线能给人们带来接近线性的主观感受。
我们先不用考虑低于30dB(A)的部分,因为在大部分环境里就已经有这么大的背景噪声了。所以我们可以将30dB(A)作为其阈值。
然后我们继续假设用户的设备最大可以产生90dB(A)的声音。这其实已经是一个蛮大的音量了,大部分人都不会想要让自己长时间暴露在大于90dB(A)的环境中的。也许手机、电脑、平板的内置喇叭都达不到这个水平,但是耳机耳塞和Hi-Fi, PA等音箱系统是可以的。
现在我们确定下我们的曲线中的两个点了,也就是
由于
现在我们得到的曲线,应该具有不错的实用性,而且在大多数情况下都能令人满意了。
理论上讲,推子推到最小的位置应该对应到30dB(A),也就是会被环境噪声遮掩掉的水平。尽管没有必要强制让这个点的输出是0,但是实际上我们还是希望它对应到0。
因为人们都会期望当音量调到最小时,声音设备不再有输出,并且我们前面做的估计工作也不可能完全准确。一个最简单的解决方法就是加一个判断条件
if(x==0) ampl=0;
如果需要更平滑地过渡到0,也可以这样子:
if(x < 0.1) ampl *= x*10;
表1给出了按照前面所讲的思路设计的,不同动态范围下理想曲线的
你可以在你的代码里直接使用这个指数方程。如果你不知道用户的设备所能达到的最大响度是多少,就猜一猜吧。上面说到的30到90,就是一个不错的猜测。
不过这种猜测永远也不可能准确,因为dB(A)也与播放的声音的种类有关。
即使我们算出来的曲线和实际要求有一定的偏差,也比一条愚蠢的线性曲线好不知道多少倍。更别说是用了刚刚说到的平滑的滚降之后的效果了。
寻找不是特别理想但是仍然有不错的效果的曲线
有些程序员可不想就为了你一个音量腿子,调用一整个数学库!所以,我们可以折衷一下,找到一条接近指数曲线,但是又不至于那么麻烦的曲线。
下图绘制出了三条曲线:线性曲线(嫌弃,略略略)、60dB指数曲线(红)和
就像你的眼睛看到的,蓝色的曲线跟红色的曲线还蛮接近的,而且你还可以看到线性的曲线错得多么离谱。
四次幂曲线的方程,每次运算只需要做三次乘法(如果你愿意多写一行代码,只做两次乘法也可),而且它经过零点。已经很优秀了吧。
我在一些设备上尝试应用了4次幂曲线,它给人的感觉十分自然,所以我强烈向你安利它。你也许也会觉得5次幂曲线更好,这取决于你自己了。
如果最大音量没有那么大,那你可以用一条不那么弯的曲线
表1的最后一列给出了各个动态范围下的曲线近似方程。你也可以在下面的图表里看看这些曲线的近似情况(下方的表格以dB作为纵坐标的单位)。
在推子比较低的位置上,由于幂次曲线降低到0(负无穷分贝)的速度要比指数曲线快很多,所以这部分的近似比较不理想。放到dB坐标中来看这个差距还是蛮大的。不过看看线性曲线的表现,就会觉得这点代价还是可以接受的了。
7次幂曲线在高达120dB动态范围的情况下保持了不错的近似,不过大部分情况下你都不应该制造可以发出100dB(A)声响的设备,免得收到因为使用你的设备而损伤了听力的诉讼。
后记
人类能够分辨出的最小的音量区别,大概是1dB,或者说10%。如果你想要做一个离散的音量控制器,比如通过点击“+-”符号来调整音量的控制器,那就最好让每次点击调整都能被感觉到。
但是你也最好不要让每次点击的变化太大,如果你的控制器跨度太大了,用户体验也不好。
一个比较推荐的经验值每次调整2dB,且最多不要超过3dB。
在GNOME一个版本的音量调整中,步长居然达到了5dB。整个网站都在抱怨这个事情。
我有时候会收到别人的邮件,问我应该怎么设置一个被设计成使用dB作为单位的音量控制器。有人依然觉得它们应该对它进行非线性变换。不不不!这种情况下你只需要设定你需要的范围和步长就行了!
举个例子,有些音量控制器给你提供了120dB的动态范围,但是如果用不上那么多,你可以把它限制到上半部分的60dB就好了。有些地方可能会同时提供衰减和增益,至于具体怎么用,你就自己多加注意啦!
有些人没有把这些对数啦,感知啦理解好,有可能会出现一些奇奇怪怪的想法。
比如说:“一个98dB(A)的声音太吵了!如果我把它减小到95dB(A),那它就只剩下原来一半的能量,只有一半吵了!”
又对又错。能量确实是只剩下一半了(振幅衰减至
上面讲的这么多,不只是适用于推子,也适用于旋钮(虽然在软件里很少见,但是大部分的硬件都用的电位器。这些电位器也都具有指数的响应曲线)和各种情况。甚至在均衡器里也是这样,即使它们的每个推子都只控制频谱中的一部分频率成分。
除了在要求极高的情况下,其实音量控制并不总是十分精确的。总之,这篇文章的中心就是,音量控制器应该做成指数响应的,或者,至少让它的响应曲线长得别差太多!
关于频率控制和频率分析
这部分是小得多的问题了,因为大部分应用都不需要在用户端处理频率。
人类对音高的感知也不是线性的,所以如果你需要对频率进行操作,也不要用线性的曲线!你必不想听到一台按照线性尺度进行调音的钢琴!
这不止在声音合成的领域有关,如果你要做声音信号分析,也会用到这些知识。如果你想要做一个频谱分析仪,如果没有特殊要求,那么它的频率轴就应该是对数的。如果你把它做成线性的,那么大部分低频都会被挤压到小小的一块空间里,而高频成分会在一大块空间里分散开来。
即使我们听觉的频率范围,上限达到了20kHz,但是大概2kHz的位置开始,我们就会觉得音高已经很高了。音乐中主要的乐器都集中在2kHz以下。而语音信号中,超过4kHz的成分已经不多了(电话就会用一个滤波器滤去这以上的频率成分)。而这些高频成分,如果在线性坐标下,会占掉近80%的空间!
不过,要生成一个对数坐标的频谱可不容易。FFT是线性的,唯一的办法就是在做完FFT后,再调整坐标轴,以对数的形式去展示数据。而这会导致低频的分辨率低得可怜,而高频成分又远超所需的分辨率。为了解决这个问题,你可以在超高的频率分辨率下来做FFT,使得低频区域获得足够的分辨率。但是这样做又会损失掉高频部分的时间分辨率。也有其他的解决办法,比如通过一系列滤波器后,对不同频率的声音分别采取不同的FFT大小。不过这样一来,各个频率成分的时间分辨率又难以统一了。
原文遵守CC BY 4.0协议
参考
- ^一个有意思的例子。2012年左右,BBC嵌入到新闻文章的视频播放器的音量控制器,可以达到11,然而在10调到11的时候,是完全听不出区别的。甚至从8调到11,能听出来的区别也很小。因为它是线性的