【翻译】线性的音量推子……简直像一个个秤砣!

——副标题:给音频软件硬件工程师的提示

原文:Programming Volume Controls - dr-lex.be

因为很多程序员缺乏对人类听觉系统的常识,或者只是因为太懒了,很多音频软件都存在一个很烦的问题。当你使用这些音频软件的音量控制的时候,会觉得痛不欲生。如果你有机会参与到音频软件的开发中去,那么请仔细阅读下面这些文本,把它提到的知识烧进自己的脑子里去,甚至跟身边的人奔走相告!

太长不看

  • 音量推子是非线性的

线性的音量控制器真的是烦死人了!因为人类对响度的感知是对数的!这也是为什么所有的音频设备都用dB来标示它的音量控制或者增益控制。

对于一个相对的振幅 x 来说(即 x=\frac{x_a}{x_0} ,其中 x_a 是当前值, x_0 是参考值),它对应的分贝值是 20\log_{10}{x} 。正的分贝值表示放大,而负的分贝值表示衰减。

将振幅乘以特定的数值,那么对应的分贝值则增加了一定的数值。

此外,dB(A)经常被用来衡量人类感知到的绝对响度。人类平均可以感知到的最小的声响,被标记为0dB(A),一个“安静”的房间大概会有±30dB(A)的声响。


  • 一个音量控制器不应该是基于百分比的

因为百分比即是线性的。

但是如果这个百分比表示的是dB值的百分比,那就还可以接收。比如0%表示-60dB而100%表示0dB。


  • 一个理想的推子应该符合指数曲线

这条曲线的公式为 y=A\cdot \exp(bx)

对于这条曲线来说,它的最小值代表“最安静”(对消费级产品来说是30dB(A)),而其最大值对应了该音频产品的最大响度。

有个问题是,你在很多情况下只能去猜测用户使用的是什么产品。所以,除非你是在做一个有明确的标准的高端产品,其实你是可以做一些猜测或者近似的。

一个很好用的估计值是用户拥有60dB的可调整范围。


  • 表1给出了一些参数a和b的经验值

表1中给出了在不同响度范围下,参数 ab 可设置的经验值供参考。

其中,x是滑块的位置,其取值范围为 [0, 1]y 则是作用于波形上的系数,对波形的振幅进行缩放。

大概率上,你会使用的是60dB范围上的参数。

如果你想要让推子推到最小的时候,得到完全不输出信号,那么你可以在接近0的地方给函数设置一个线性的滚降(roll-off)

表1:方程中a和b的值
  • 不连续的控制器

如果你的音量控制器是离散的,比如说,用鼠标点击按钮、或者鼠标滚轮来控制,那么最好确保每两“格”音量之间的区别在1~3dB之间。

小于1dB的音量变化难以被察觉到,而3dB以上的变化则太过于粗糙了。2dB是一个蛮不错的选择。


  • 近似曲线

如果出于某些原因,你不想做指数运算,那你可以用一条所需的运算量低得多的幂次曲线来代替。

对于典型的60dB范围,这是一条4次曲线。换句话说,让波形振幅乘以系数 x^4

表1中也给出了不同动态范围下适用的近似曲线。虽然这些曲线不如对数曲线那么完美,但是再怎么说都好过你直接塞个线性的音量推子进去!

本文比较长,正文的前半部分都在讲关于分贝的基础知识。如果你只想看关于音量推子的部分,可以跳过前半部分,从“寻找理想的曲线”一节开始阅读。

为什么万恶的线性的音量控制器是邪恶的

当今的大部分音频软件都会带有控制音量的推子或者旋钮,其目的是模仿传统的音频硬件。不幸的是,大部分的这些软件控制器上都会有一点让人屁眼疼。那就是它们是线!性!的!

你也许会问:一个线性的推子犯了什么错?一端是0,一端是100,中间呈线性增长,不是很棒吗?答案是不不不不不。

你可以这样子试试看:打开一个音频软件,播一首你喜欢的曲子。把音量旋钮打到最大,然后稍稍搓动它。接着,把音量调到最小,然后稍稍搓动它。

如果这时候你听到,在音量最大的那一端基本没什么音量变化,而在最小的那一端疯狂变化。那么这个控制器十有八九就是线性的控制器了![1]

(如果你身边的播放器都做了正确的事情,你可以看看译者补充的这个视频感受下区别。)

https://www.zhihu.com/video/1227903653497257984

这种邪恶甚至已经渗透到一些硬件上了!Velleman卖过一款可以拆卸组装的图形均衡器K4302,在我1995年买到的版本上,搭载的推子就是线性的,也不知道现在修正了没有。
甚至iMac G3上的音量控制器也是线性的。
恐怕这些只是众多使用了线性推子的硬件中的一角。

除了上面提到的,线性的音量控制器还会导致这些症状:

  • 如果你从最小的音量网上推,只要推一点点就已经太大声了
  • 从差不多中间开始,到最大音量,几乎没什么变化
  • 如果一个高灵敏度的耳机和低输出的音响接到同一台电脑上,基本不可能对耳机进行微调。因为要拖动很大很大的距离,才能让音响出现音量的改变

种种这些问题,就会让用户觉得你的产品难用,但是又找不出为什么,最后只能烦到大骂:

这些音量推子……简直就像一个个秤砣!

问题出在哪里?

所以线性的音量推子到底怎么了?其实是因为我们对声音的感知是对数的。

横轴:振幅;纵轴:感知响度

也就是说,相同的振幅变化,在振幅本身就小的时候,我们感知到的变化程度要比振幅本身比较大的时候更大。
这种特性让我们能既能听到振幅很小的声音,也能听到振幅超大的声音。或者说,我们的听觉覆盖了很大的动态范围。

而放到音量控制上,线性的推子给出的音量变化,在我们听来是呈对数变化的,所以会觉得怪怪的。上图就是一条对数曲线。横轴上标记了两个完全一样长度的区域(也即线性的音量推子)。而纵轴则显示了听觉上的音量变化。在音量较小的那段,变化的程度要比较大的那端大。

为了得到一个“真正的”音量推子,我们只需要让推子的输出是指数的。因为 \log(\exp(x))\propto x ,这样一来,主观感知上的音量变化就是线性的了,而这才是我们想要的。

在下文中,我假设音量推子和整个音频系统,都工作在 [0, 1] 的值域上。且用 x 来表示音量推子的位置, y 来表示于波形数据相乘的系数。

寻找理想的曲线

指数曲线有两个很烦的属性:

  • 一是只有自变量是负无穷时,它的值才是0

不过这个问题不大,因为我们的耳朵并没有拥有无限的敏感度。我们只需要知道一些实用的动态范围就行了。这部分在下面再展开讲。

  • 二是指数函数的一般形式 y=A\cdot \exp(bx)+c 的图像,即使确定了两个点之后,还是无法定下具体的三个参数。

所以在音量控制的场景中,我们省去 c 这个参数,因为耳朵无法感知到直流偏移(译者注:感觉这个解释不是很好,加上 c 应该是整体的音量都提升或衰减,是可以被感知的)。这样一来,就可以通过两个点来确定一条具体的曲线了。

我们要得到的曲线,一定会经过 (1, 1) 这个点。于是可以得到 A=\frac{1}{\exp(b)}

剩下的问题就是,确定控制曲线形状的 b 值。更小的值生成的曲线更加陡峭,而更大的值生成的曲线则更加平缓。


你也许会尝试把第二个点选择为 (0, 0) ,但是不应该是这样的。就像刚刚说的,指数函数在自变量是0的时候可不是0。不过这也不是大问题,因为我们耳朵的对数响应曲线,会在一个非零响度的地方达到0,也即听阈(The Hearing Threshold)

一般的环境中,都存在一定的背景噪声。如果一个声音的响度低于这个背景噪声的响度,就可以是不可闻的了。最大的问题是,即使不同人的听阈大致上是一致的,但是不同的音响系统在相同的输入信号下,输出的响度却和大量的参数相关。

为了得到合适的 b 值,我们需要更多的信息。如果我们想要让用户在调节音量时的感觉完全是线性的,我们就需要知道一个音频系统在最大音量时的响度是多少。但是很显然,这个问题没有什么实用性。除非你是给特定的硬件系统开发软件,否则这个问题很简单,就是没有确切的答案。

所以我们就需要做一些假设。这里插一个题外话,就是“响度”是如何被测量的。

声强的测量

因为人类的听系统具有对数的响应曲线,人类定义了一个特殊的单位,以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。其计算方法与你要计算的物理量是振幅还是功率有关。

对于功率值来说,其公式为 10\log_{10}(x)x 代表相对的功率(即两个功率的比值)
对于振幅来说,其公式为 20\log_{10}(x)

产生这种区别的原因是,功率正比于振幅的平方。在对数运算中,平方可以提取到对数运算符的外面,变成系数2.

理论上来说,绝对标度和相对标度不能真正地互换。当我们对一个90dB(A)的声音衰减20dB,我们其实无法保证得到的声音就是精确的70dB。(译者不太理解这一点)

寻找理想的曲线(第二部分)

在我们学习了这么多关于分贝的度量的知识之后,我们可以继续刚刚寻找两个参数的问题了。我们要确保这条曲线能给人们带来接近线性的主观感受。

我们先不用考虑低于30dB(A)的部分,因为在大部分环境里就已经有这么大的背景噪声了。所以我们可以将30dB(A)作为其阈值。

然后我们继续假设用户的设备最大可以产生90dB(A)的声音。这其实已经是一个蛮大的音量了,大部分人都不会想要让自己长时间暴露在大于90dB(A)的环境中的。也许手机、电脑、平板的内置喇叭都达不到这个水平,但是耳机耳塞和Hi-Fi, PA等音箱系统是可以的。

现在我们确定下我们的曲线中的两个点了,也就是 {\rm (0, 30dB(A))}{\rm (1, 90dB(A))} 。常用的音量推子控制的都是衰减的值。如果我们把这条曲线放到相对坐标轴里,得到的点就是 {\rm (0, -60dB)}{\rm (1, 0dB)} 了。为了让计算更加直观,可以给它添加一个60dB的偏移,得到 {\rm (0, 0dB)}{\rm (1, 60dB)} 两个点。从振幅上看,60dB是0dB的 10^{60/20}=1000 倍。

由于 1000=\exp(b\cdot1) , b=\ln(1000)\approx 6.908 ,那么a的值就是简单的 1/1000 了。


现在我们得到的曲线,应该具有不错的实用性,而且在大多数情况下都能令人满意了。

理论上讲,推子推到最小的位置应该对应到30dB(A),也就是会被环境噪声遮掩掉的水平。尽管没有必要强制让这个点的输出是0,但是实际上我们还是希望它对应到0。

因为人们都会期望当音量调到最小时,声音设备不再有输出,并且我们前面做的估计工作也不可能完全准确。一个最简单的解决方法就是加一个判断条件

if(x==0) ampl=0;

如果需要更平滑地过渡到0,也可以这样子:

if(x < 0.1) ampl *= x*10;

表1给出了按照前面所讲的思路设计的,不同动态范围下理想曲线的 ab 的值。(这里的动态范围就是最大响度和背景噪声的差值)

你可以在你的代码里直接使用这个指数方程。如果你不知道用户的设备所能达到的最大响度是多少,就猜一猜吧。上面说到的30到90,就是一个不错的猜测。

不过这种猜测永远也不可能准确,因为dB(A)也与播放的声音的种类有关。

即使我们算出来的曲线和实际要求有一定的偏差,也比一条愚蠢的线性曲线好不知道多少倍。更别说是用了刚刚说到的平滑的滚降之后的效果了。

寻找不是特别理想但是仍然有不错的效果的曲线

有些程序员可不想就为了你一个音量腿子,调用一整个数学库!所以,我们可以折衷一下,找到一条接近指数曲线,但是又不至于那么麻烦的曲线。

下图绘制出了三条曲线:线性曲线(嫌弃,略略略)、60dB指数曲线(红)和 x^4 (蓝)。

就像你的眼睛看到的,蓝色的曲线跟红色的曲线还蛮接近的,而且你还可以看到线性的曲线错得多么离谱。

四次幂曲线的方程,每次运算只需要做三次乘法(如果你愿意多写一行代码,只做两次乘法也可),而且它经过零点。已经很优秀了吧。

我在一些设备上尝试应用了4次幂曲线,它给人的感觉十分自然,所以我强烈向你安利它。你也许也会觉得5次幂曲线更好,这取决于你自己了。

如果最大音量没有那么大,那你可以用一条不那么弯的曲线 x^3 ,或者如果你的动态范围更大,你也可以选择比 x^4 更弯的曲线。比如对于90dB动态范围的系统来说, x^6 才是比较好的近似。不过需要用到这么弯的曲线的系统其实不多。

表1的最后一列给出了各个动态范围下的曲线近似方程。你也可以在下面的图表里看看这些曲线的近似情况(下方的表格以dB作为纵坐标的单位)。

在推子比较低的位置上,由于幂次曲线降低到0(负无穷分贝)的速度要比指数曲线快很多,所以这部分的近似比较不理想。放到dB坐标中来看这个差距还是蛮大的。不过看看线性曲线的表现,就会觉得这点代价还是可以接受的了。

7次幂曲线在高达120dB动态范围的情况下保持了不错的近似,不过大部分情况下你都不应该制造可以发出100dB(A)声响的设备,免得收到因为使用你的设备而损伤了听力的诉讼。

后记

人类能够分辨出的最小的音量区别,大概是1dB,或者说10%。如果你想要做一个离散的音量控制器,比如通过点击“+-”符号来调整音量的控制器,那就最好让每次点击调整都能被感觉到。

但是你也最好不要让每次点击的变化太大,如果你的控制器跨度太大了,用户体验也不好。

一个比较推荐的经验值每次调整2dB,且最多不要超过3dB。

在GNOME一个版本的音量调整中,步长居然达到了5dB。整个网站都在抱怨这个事情。

我有时候会收到别人的邮件,问我应该怎么设置一个被设计成使用dB作为单位的音量控制器。有人依然觉得它们应该对它进行非线性变换。不不不!这种情况下你只需要设定你需要的范围和步长就行了!

举个例子,有些音量控制器给你提供了120dB的动态范围,但是如果用不上那么多,你可以把它限制到上半部分的60dB就好了。有些地方可能会同时提供衰减和增益,至于具体怎么用,你就自己多加注意啦!

有些人没有把这些对数啦,感知啦理解好,有可能会出现一些奇奇怪怪的想法。

比如说:“一个98dB(A)的声音太吵了!如果我把它减小到95dB(A),那它就只剩下原来一半的能量,只有一半吵了!”

又对又错。能量确实是只剩下一半了(振幅衰减至 \frac{\sqrt{2}}{2} 倍),但是主观感知只减少了3dB,也就是只比能感受到的变化多那么一点点。因为这就是分贝这个单位本身被提出来的意义呀!在这种情况下,功率对半减少了,对听力的损伤可没有减少多少。

上面讲的这么多,不只是适用于推子,也适用于旋钮(虽然在软件里很少见,但是大部分的硬件都用的电位器。这些电位器也都具有指数的响应曲线)和各种情况。甚至在均衡器里也是这样,即使它们的每个推子都只控制频谱中的一部分频率成分。

除了在要求极高的情况下,其实音量控制并不总是十分精确的。总之,这篇文章的中心就是,音量控制器应该做成指数响应的,或者,至少让它的响应曲线长得别差太多!

关于频率控制和频率分析

这部分是小得多的问题了,因为大部分应用都不需要在用户端处理频率。

人类对音高的感知也不是线性的,所以如果你需要对频率进行操作,也不要用线性的曲线!你必不想听到一台按照线性尺度进行调音的钢琴!

这不止在声音合成的领域有关,如果你要做声音信号分析,也会用到这些知识。如果你想要做一个频谱分析仪,如果没有特殊要求,那么它的频率轴就应该是对数的。如果你把它做成线性的,那么大部分低频都会被挤压到小小的一块空间里,而高频成分会在一大块空间里分散开来。

即使我们听觉的频率范围,上限达到了20kHz,但是大概2kHz的位置开始,我们就会觉得音高已经很高了。音乐中主要的乐器都集中在2kHz以下。而语音信号中,超过4kHz的成分已经不多了(电话就会用一个滤波器滤去这以上的频率成分)。而这些高频成分,如果在线性坐标下,会占掉近80%的空间!

不过,要生成一个对数坐标的频谱可不容易。FFT是线性的,唯一的办法就是在做完FFT后,再调整坐标轴,以对数的形式去展示数据。而这会导致低频的分辨率低得可怜,而高频成分又远超所需的分辨率。为了解决这个问题,你可以在超高的频率分辨率下来做FFT,使得低频区域获得足够的分辨率。但是这样做又会损失掉高频部分的时间分辨率。也有其他的解决办法,比如通过一系列滤波器后,对不同频率的声音分别采取不同的FFT大小。不过这样一来,各个频率成分的时间分辨率又难以统一了。


原文遵守CC BY 4.0协议

参考

  1. ^一个有意思的例子。2012年左右,BBC嵌入到新闻文章的视频播放器的音量控制器,可以达到11,然而在10调到11的时候,是完全听不出区别的。甚至从8调到11,能听出来的区别也很小。因为它是线性的
编辑于 03-30

文章被以下专栏收录