去年写过一篇文章,有关PCM的音量控制:http://blog.jianchihu.net/pcm-volume-control.html。那时阐述了一些概念,对一些细节没有详细描述。因为有人问到使用对数关系调节音量,故开此篇文章。
声学中的分贝
因为人耳的特性,我们对声音的大小感知呈对数关系。所以我们通常用分贝描述声音大小,分贝(decibel)是量度两个相同单位之数量比例的单位,主要用于度量声音强度,常用dB表示。声学中,声音的强度定义为声压。计算分贝值时采用20微帕斯卡为参考值(通常被认为是人类的最少听觉响应值,大约是3米以外飞行的蚊子声音)。这一参考值是人类对声音能够感知的阈值下限。声压是场量,因此使用声压计算分贝时使用下述版本的公式:
其中的pref是标准参考声压值20微帕。
分贝声音变化范围
在编程中,我们可以用以下公式计算两个声音之间的动态范围,单位为分贝:
1 |
dB = 20 * log(A1 / A2) |
其中 A1 和 A2 是两个声音的振幅,在程序中表示每个声音样本的大小。声音采样大小(也就是量化深度)为1bit时,动态范围为0,因为只可能有一个振幅。采样大小为8bit也就是一个字节时,最大振幅是最小振幅的 256 倍。因此,动态范围是 48 分贝,计算公式如下:
dB = 20 * log(256)
48 分贝的动态范围大约是一个安静房间和一台运行着电动割草机之间的区别。如果将声音采样大小增加一倍到16bit,产生的动态范围则为 96 分贝,计算公式如下:
dB = 20 * log(65536)
这非常接近听力最低阈值和产生痛感之间的区别,这个范围被认为非常适合还原音乐。
了解了分贝的相关概念我们通过图表说下为什么要用对数关系描述声音大小。
1)音量滑块与声音增幅大小线性变化。
上述左图中,音量滑块位置与声音振幅为线性增长关系,右图是我们人耳感受的音量大小与滑块位置关系。可知,在左侧移动相同距离的滑块,感知到的声音变化范围很大,在右侧接近声音最大值移动相同距离滑块,感知到的声音大小变化就很小了。
2)音量滑块与声音振幅大小对数关系变化。
左图中,音量滑块位置与声音振幅对数关系增长。右图中无论哪个位置,移动相同距离滑块,感知到的声音变化都是相同的。
需要说明的是滑块最小位置只是接近0,不能为0,因为对数函数y=logx中x>0。
windows系统中音量滑块控制的声音变化范围
在最新版的windows系统中,音量滑块控制的声音变化范围也是96分贝。如下表所示,是不同版本windows的音量范围以及默认音量值。
从表中我们可以看到默认值都是0分贝,根据分贝公式:dB = 20 * log(A1 / A2)
,当A1,A2相等时,db为0。
程序实现
了解了分贝以及Windows中音量滑块是在哪个范围变化,我们的程序实现起来也很简单。
这里我们规定音量大小变化范围也是96分贝,每个声音采样大小为16位。对于分贝公式:dB = 20 * log(A1 / A2),我们取参考声音振幅A2为原始声音振幅,A1为调节后的声音振幅大小。可知调节后的声音:
1 |
A1 = A2 * pow(10 , db/20) |
看过一篇文章说理想的声音调节步长最好是2db,对于96db范围,我们按2db步长进行分割,可以分成48份,这样我们得到的声音变化为[-96db,-94db,-92db,...-4db.-2db,0db],假设我们要调节一半音量大小,也就是-48db,由上述公式可知:调节后音量A1大小:
1 |
A1 = A2 * pow(10 , -48/20) |
程序伪代码如下,具体db大小与滑块位置对应关系的实现这里就不写出:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int16_t pcm[1024] = read in some pcm data; int32_t pcmval; float multiplier = pow(10,db/20); for (ctr = 0; ctr < 1024; ctr++) { pcmval = pcm[ctr] * multiplier; if (pcmval < 32767 && pcmval > -32768) { pcm[ctr] = pcmval } else if (pcmval > 32767) { pcm[ctr] = 32767; } else if (pcmval < -32768) { pcm[ctr] = -32768; } } |
参考
[1] Audio-Tapered Volume Controls.https://docs.microsoft.com/en-us/windows/win32/coreaudio/audio-tapered-volume-controls
文章评论
可以算音量大小吗
@Vincenzo
音量大小不就是振幅大小
不知道为什么,会有杂音。。。。
@parcool
调整前就有杂音还是调整后?
@Jianchihu 调整后
@He.ToSion
话说调整音量并做溢出处理并不会引入噪音,我怀疑还是你的原始数据有问题。看下你pcm数据格式是否转换正确,比如:整形非整形数据转换,不同精度数据转换,字节序等等。这些都可能造成部分音频数据丢失,从而引入噪音。
你好,我看了几遍文章,还是没明白最后代码中的db该怎么计算获得。比如我想将当前PCM数据的音量调小到80%(降低20%),按照以前的方式就是 pcmval = pcm[ctr] * 80%。按照您这种方式应该怎么把这个80%转换成合理的multiplier呐?
@sens
db这个单位是相对值,得有一个参考,对于公式:dB = 20 * log(A1 / A2),我们取参考声音振幅A2为原始声音振幅,A1为调节后的声音振幅大小。按你的说法,A2 = pcm[ctr],A1 = pcmval 。
@Jianchihu 是的,这个理解了。但是滑块怎么和db关联起来呐?您最后给出了:
float multiplier = pow(10, db/20);。比如说我滑块的取值范围是M=[0,100],0表示静音,100表示原始声音大小。这个db该怎么跟随滑块的变化而变化呐?
@sens
1)我们声音取值范围一般是-96db~0db(参考windows系统设计),0db就是原始声音大小,-96db是声音最小
2)滑块取值范围0~100,0就对应-96db,100对应0db,其他滑块值对应的db值按线性关系类推
3)套入公式:db = 20 * log(pcmval / pcm[ctr]),其中db取值-96~0db,其他的你应该明白了吧
@Jianchihu 谢谢你的解释,大致明白了。但是我发现这样子计算的变化弧度还是有些问题。比如我想把音量调到70%(在原来的基础上降低30%):
db = -96 * (1 - 0.7) ≈ -29
multiplier = pow(10, db / 20) = pow(10, -29 / 20) ≈ 0.03
也就是音量只想减小到70%,但是通过上面的计算,振幅则只有原来的3%了。这个减小的幅度太大了吧。这样小于60%后基本就听不到声音了。
@sens 我说的滑块值对应的db值按线性处理只是个参考,你可以通过计算multiplier大小得到合适的对应关系
如果需要将已有的pcm数据做增幅,需要如何处理呢?
@廖生 原理不是一样的吗?对一个个pcm样本处理。
在windows中调节滑块,音量并不是根据dB线性变化的,以总步进数为100为例,总步进数越小,每个步进的dB数要大,总步进数接近100的时候,每个步进数越来越小。不知道为什么
@jj 请参考:https://docs.microsoft.com/en-us/windows/win32/coreaudio/audio-tapered-volume-controls。
> A Windows application that displays a volume slider defines a relationship between the slider position and the output signal level at the speakers. The relationship can, in effect, be linear tapered or audio tapered.
您好!看了您的文章,很详细,学到很多。因为是初学,有一个问题请教您,我从视频中提取的音频数据格式是PCM的,然后需要提取其中的特征,请问您了解essentia库可以直接对PCM格式的音频数据进行特征提取吗?谢谢您的解答。我只找到一个有关资料,https://github.com/MTG/essentia/issues/775
@滴水藏海 没了解过这方面内容
您好!
请教一点关于pcm文件解析的问题,方便邮件沟通吗?
不甚感激!
@程序同學 抱歉,现在已经不搞这方面了
音量我看其他网上的介绍不是20*lg(p1/p0);其中p0是人耳在1000hz下能听到最小声音的声压,p1是待计算音量的声压;为啥这里介绍时振幅呢?