-
Notifications
You must be signed in to change notification settings - Fork 163
软件音量算法
通过 ffmpeg 可以解码出原始的音频数据,在将音频数据送到设备播放之前,我们就可以利用软件算法对音量进行控制。
音频数据本质就是一个个的采样点,每个采样点一般情况就是一个 signed 16bit 整型数。要对其进行音量控制其实很简单,就是乘以一个缩放因子。当然音量放大的时候,乘以一个缩放因子还不够,因为运算可能会有溢出,就需要做一下饱和处理:
int16_t isample = 23456;
double factor = 4.0;
double fsamnew;
int16_t isamnew;
fsamnew = isample * factor; // 乘以缩放因子
//++ 饱和处理
fsamnew = fsamnew < 0x7fff ? fsamnew : 0x7fff;
fsamnew = fsamnew >-0x7fff ? fsamnew :-0x7fff;
//-- 饱和处理
isamnew = (int16_t) fsamnew;
以上代码是对单个 sample 进行缩放运算,音频数据一般是存放在一个数组里的,只要把上面代码再套一个 for 循环,遍历数组处理完全部的采样点就可以了。
我们可以参考 fanplayer 中 adev_post 函数的代码:
void adev_post(void *ctxt, int64_t pts)
{
if (!ctxt) return;
ADEV_CONTEXT *c = (ADEV_CONTEXT*)ctxt;
c->ppts[c->tail] = pts;
//++ software volume scale
int multiplier = c->vol_scaler[c->vol_curvol];
SHORT *buf = (SHORT*)c->pWaveHdr[c->tail].lpData;
int n = c->pWaveHdr[c->tail].dwBufferLength / sizeof(SHORT);
if (multiplier > (1 << 14)) {
int64_t v;
while (n--) {
v = ((int32_t)*buf * multiplier) >> 14;
v = v < 0x7fff ? v : 0x7fff;
v = v >-0x7fff ? v :-0x7fff;
*buf++ = (SHORT)v;
}
} else if (multiplier < (1 << 14)) {
while (n--) {
*buf = ((int32_t)*buf * multiplier) >> 14; buf++;
}
}
//-- software volume scale
waveOutWrite(c->hWaveOut, &c->pWaveHdr[c->tail], sizeof(WAVEHDR));
if (++c->tail == c->bufnum) c->tail = 0;
}
在将音频数据写到 waveout 设备之前(waveOutWrite 之前),我们使用了这个软件缩放算法,来实现软件音量控制功能。
算法中 0x7fff 就是最大的 16bit 有符号正整数,超过这个值我们就保持这个最大值不变;-0x7fff 是最小的 16bit 有符号负整数,小于这个值我们就保持这个最小值不变。这就是所谓的饱和运算。其作用是为了防止运算溢出,如果不做这样的处理,溢出后的运算数据,播放出来会有明显的杂音。饱和运算在现代的处理器上,一般都有专门的汇编指令可以进行优化,可提高运算速度。fanplayer 中没有做汇编优化,只是给出了一个 C 语言的实现算法。在 fanplayer 的实现中,使用了简单的定点乘法运算进行优化,避免了浮点运算,在一定程度上也保证了效率。
如何设计一个软件音量的控制器呢?我们一般都是以 dB 来表示音量的增益或衰减,fanplayer 中设计了一个 -30dB ... +12dB 的音量控制器(最大可以放大 2 倍,衰减 5 倍)。因为日常使用中我们调小音量比较多些,所以衰减设计得比较大了一些。与 dB 值相对应的,就是 0 - 255 总共 256 个音量等级,可以用一个 uint8_t 的整数来表示。[0 ... 255] 与 [-30dB ... +12dB] 是一个线性的一一对应的关系。所以每增加一个 step 音量增加大约为 (12 + 30) / 256 = 0.1640625 dB。
dB 表示的是一个比例关系,dB 的定义如下:
dB = 20 * log(x / vf); // vf 为参考常量
vf 已知,如果知道 x 的值,通过这个公式就可以算出一个 dB 值,这个 dB 值就表示了 x 与 vf 之间的比例关系。
例如:
a) 当 x 等于 vf 时,那么:
20 * log1 = 0dB
此时它们是 0dB 的关系,也可以说 vf 增益或者衰减 0dB 后,值仍然是 vf
b) 当 x 等于 2*vf 时,那么:
20 * log2 = 6.0205999132796239042747778944899
此时他们是 6dB 的关系,也可以说 vf 增益 6dB 后得到 x
那么我们已知 dB 值,如何求出一个衰减或增益的缩放因子?
dB = 20 * log(x / vf);
=> (dB / 20) = log (x / vf);
=> 10 ^ (dB / 20) = x / vf;
=> x = 10 ^ (dB / 20) * vf;
=> factor = 10 ^ (dB / 20); x = factor * vf;
这样我们就有了 factor 的计算公式。前面我们已经得到了 [0 ... 255] 与 [-30dB ... +12dB] 的一一对应,那么很容易就得到 [0 ... 255] 与 [factor0 ... factor255] 的一一对应。
可以用如下算法,计算出这张 factor 表:
int maxdb = +12;
int mindb = -30;
double tabdb[256];
double tabf [256];
int z, i;
for (i=0; i<256; i++) {
tabdb[i] = mindb + (maxdb - mindb) * i / 256.0;
tabf [i] = pow(10.0, a[i] / 20.0);
}
tabf 这个数组保存的就是 factor 表了,我们可以在初始化的时候预先计算好。(参考 fanplayer 源代码的 init_software_volmue_scaler 函数)
当我们调整音量时,只需要根据当前音量,索引出 factor 值,然后将每个音频采样点乘以这个 factor 值,就实现了软件音量控制。
我们再来看看定点乘法优化:
int16_t sample = 123;
double factor = 1.2;
123 * 1.2 = 123 * 1.2 * 0x10000 / 0x10000 约等于 (123 * (1.2 * 0x10000)) >> 16 约等于 (123 * 78643) >> 16
这里 1.2 * 0x10000 就是 Q16 的一个定点数,也可以说,浮点数 1.2 的 Q16 的定点表示法为 1.2 * 0x10000 = 78643. 这样我们利用定点运算,可以将浮点的乘法运算,转换为整数的乘法和移位运算。
方法:
-
首先根据数据范围和精度需求确定 Q 值,原则是避免溢出的情况下追求尽量高的精度。
-
将浮点类型的 factor 转换为定点类型,方法为:
浮点数 x,转换为 Qx 的定点数 X
X = (int)(x * (1 << Qx)); -
进行定点乘法运算,运算方法为:
浮点运算 z = x * y,对应定点数 Z X Y,Q 值分别为 Qz Qx Qy:
则 Z = X * Y >> (Qx + Qy - Qz)
在 fanplayer 源代码中,选用了 Q14。因为最大增益为 12dB,大概就是放大 4 倍,选用 Q14 能保证 32bit 的定点运算不会溢出。 因此在源代码中,根据 double 型的 factor 计算出了 Q14 的定点 scaler 因子,然后在 adev_post 中运用定点乘法进行了计算。 (请参考源代码的 init_software_volmue_scaler 和 adev_post 函数)
Q14 的情况下
浮点转定点方法为:
scaler = (int)(factor * (1 << 14));
乘法运算的计算方法:
samnew = ((int32_t)sample * scaler) >> 14;
本文主要讲了音量算法,dB 值的概念,还有定点运算,需要简单的数学知识和数学计算。为什么用 dB 值的概念,为什么搞这么复杂?是因为如果不按等 dB 的 step 去进行衰减或增益(等比关系),而是线性化比例去衰减或增益(等差关系),比如 1 对应 0.1 倍,2 对应 0.2 倍,3 对应 0.3 倍,100 对应 10.0 倍,这样的方式去做,给人耳的感觉是音量变化是不均匀的。为什么又要搞出一个 factor 表格,还要搞出定点运算?没办法,这些都是为了做优化……
fanplayer wiki