音频Balance源码总结
何为音频Balance?
顾名思义,Balance及平衡,平衡也就是涉及多方,音频左右甚至四通道,调节所有通道的音量比,使用户在空间内听到各个通道的音频大小不一,好似置身于真实环境中;
博主分析的Balance源码在
./system/media/audio_utils/include/audio_utils/Balance.h
./system/media/audio_utils/Balance.cpp
Balance原理
如下图,提供给用户一个设置进度条:
用户设置balance在-0.5,那就给channel left的音量为1,channel right的音量x(0~1,通过一定算法计算得到),最后我们遍历buffer中的每一个音频数据,左侧通道乘1就会保持不变,右侧乘小于1的数据,音量就会减小,这样就达成了左右channel输出的音频不同了
如上原理,很简单吧!但是我们要考虑几个问题
- 音频数据的格式类别(双通道、2.1通道的数据格式)
- 各个通道的音频分量如何确定?
辨别音频数据格式类别
这个也是Balance类第一步要做的事情,对应代码中,在执行Balance之间必须要先调用
void setChannelMask(audio_channel_mask_t channelMask);
插播一个知识点,在ChannelMask中,表示音频数据存储方式有以下2种:
- AUDIO_CHANNEL_REPRESENTATION_POSITION
位置表示法,音频数据中每个bit位表示每个位置的音频数据,如前左 前右等音响 - AUDIO_CHANNEL_REPRESENTATION_INDEX
序号法表示,每个bit位表示不同的通道,如0位是主通道,1位是辅助通道等
用audio_channel_mask_get_representation(channelMask)函数可以得到mask是用哪种方法表示;
接着上setChannelMask看,主要根据channelMask不同表示方法进行不同的处理:
深入代码,看看Balance如何区分每个channel的情况:
AUDIO_CHANNEL_REPRESENTATION_INDEX处理方式
void Balance::setChannelMask(audio_channel_mask_t channelMask)
{
//去掉haptic震动反馈通道,这个不属于balance范畴
channelMask &= ~ AUDIO_CHANNEL_HAPTIC_ALL;
//如果部署输出类型的mask,或者与之前的mask相同,就没必要再次设置
if (!audio_is_output_channel(channelMask) // invalid mask
|| mChannelMask == channelMask) { // no need to do anything
return;
}
mChannelMask = channelMask;
mChannelCount = audio_channel_count_from_out_mask(channelMask);
// save mBalance into balance for later restoring, then reset
const float balance = mBalance;
mBalance = 0.f;
// reset mVolumes将vector数组重置大小为mChannelCount
mVolumes.resize(mChannelCount);
//填充mVolumes所有值为1.f
std::fill(mVolumes.begin(), mVolumes.end(), 1.f);
// reset ramping variables
mRampBalance = 0.f;
mRampVolumes.clear();
//如果mChannelMask是按照序号法来表示,如index 0表示主通道、1副通道,
//则不是按照声场位置表示法AUDIO_CHANNEL_REPRESENTATION_POSITION
if (audio_channel_mask_get_representation(mChannelMask)
== AUDIO_CHANNEL_REPRESENTATION_INDEX) {
mSides.clear(); // mSides unused for channel index masks.
setBalance(balance); // recompute balance
return;
}
}
以上函数主要做了以下几件事情:
- 从ChannelMask中获取通道数,并根据通道数resize重置mVolumes(vector类型)大小为channelCount,因为balance就是根据通道来进行加减乘除的
- 如果channelMask是AUDIO_CHANNEL_REPRESENTATION_INDEX格式,也就是index方式来表示通道数据,无需用mSide位置来表示
- setBalance为每个声道设置对应的音量,后面展开
AUDIO_CHANNEL_REPRESENTATION_POSITION处理方式
以下这段代码也是在setChannelMask方法中的,是当channelMask为AUDIO_CHANNEL_REPRESENTATION_POSITION的处理方式
//sideFromChannel也就是把每一个channel映射到声场的位置,0-left,1-right,2-center
//比如以下为9x1u的channel对应左边的声场位置,其他依次类推
static constexpr int sideFromChannel[] = {
0, // AUDIO_CHANNEL_OUT_FRONT_LEFT = 0x1u,
1, // AUDIO_CHANNEL_OUT_FRONT_RIGHT = 0x2u,
2, // AUDIO_CHANNEL_OUT_FRONT_CENTER = 0x4u,
2, // AUDIO_CHANNEL_OUT_LOW_FREQUENCY = 0x8u, //低频分量的数据,专门传输到低音炮的外放装置中
0, // AUDIO_CHANNEL_OUT_BACK_LEFT = 0x10u,
1, // AUDIO_CHANNEL_OUT_BACK_RIGHT = 0x20u,
0, // AUDIO_CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 0x40u,
1, // AUDIO_CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x80u,
2, // AUDIO_CHANNEL_OUT_BACK_CENTER = 0x100u,
0, // AUDIO_CHANNEL_OUT_SIDE_LEFT = 0x200u,
1, // AUDIO_CHANNEL_OUT_SIDE_RIGHT = 0x400u,
2, // AUDIO_CHANNEL_OUT_TOP_CENTER = 0x800u,
0, // AUDIO_CHANNEL_OUT_TOP_FRONT_LEFT = 0x1000u,
2, // AUDIO_CHANNEL_OUT_TOP_FRONT_CENTER = 0x2000u,
1, // AUDIO_CHANNEL_OUT_TOP_FRONT_RIGHT = 0x4000u,
0, // AUDIO_CHANNEL_OUT_TOP_BACK_LEFT = 0x8000u,
2, // AUDIO_CHANNEL_OUT_TOP_BACK_CENTER = 0x10000u,
1, // AUDIO_CHANNEL_OUT_TOP_BACK_RIGHT = 0x20000u,
0, // AUDIO_CHANNEL_OUT_TOP_SIDE_LEFT = 0x40000u,
1, // AUDIO_CHANNEL_OUT_TOP_SIDE_RIGHT = 0x80000u,
};
mSides.resize(mChannelCount);
for (unsigned i = 0, channel = channelMask; channel != 0; ++i) {
//计算channel从低位开始第一个1的位置
const int index = __builtin_ctz(channel);
if (index < std::size(sideFromChannel)) {
mSides[i] = sideFromChannel[index];
} else {
mSides[i] = 2; // consider center
}
channel &= ~(1 << index);
}
setBalance(balance); // recompute balance
可以看到android定义的channel的位置有很多的,如AUDIO_CHANNEL_OUT_FRONT_LEFT、AUDIO_CHANNEL_OUT_BACK_RIGHT、AUDIO_CHANNEL_OUT_TOP_SIDE_LEFT等等,但是这么多的位置的喇叭,对应到sideFromChannel里面去的取值,只有0、1、2,也就是left、right和center;相当于把上面20个位置转换到3个位置上去,多维转换到一维了;(显然这么做是不合理的,Android这么做后期肯定是可以优化的)
最后面的for循环,就是记录当前的channelMask的几个channel对应那几个位置side,mSide数组就是保存了channel的位置side,用以下一幅图表示就是:
确定每个通道的音频音量
接上面代码,当确定好每个channel的位置后,接下来就是为每个channel计算他的音量了,也就是setBalance
//balance参数取值范围在[-1,1],这个函数的意义在于为每个channel计算它的音量值
void Balance::setBalance(float balance)
{
//如何这次设置的balance和上次相同
if (mBalance == balance // no change
//balance值非法,或者绝对值大于1,就认为非法
|| isnan(balance) || fabs(balance) > 1.f) { // balance out of range
return;
}
mBalance = balance;
//通道数小于2个也没必要
if (mChannelCount < 2) {
return;
}
//如果音频是双通道或者是用INDEX表示音频数据格式
if (mChannelMask == AUDIO_CHANNEL_OUT_STEREO
|| audio_channel_mask_get_representation(mChannelMask)
== AUDIO_CHANNEL_REPRESENTATION_INDEX) {
//那就只需要计算左右两个通道的音量值,mVolumes[0]保存左声道的音量,mVolumes[1]保存右声道的音量
computeStereoBalance(balance, &mVolumes[0], &mVolumes[1]);
return;
}
//side位置表示法格式的音频,则需要计算3个位置的音量
//计算好当前声道平衡balance对应在left、right和center的音量值
float balanceVolumes[3]; // left, right, center
//同上这里只计算左右left\right声道的音量值,中间的取音频本身的音量值
computeStereoBalance(balance, &balanceVolumes[0], &balanceVolumes[1]);
balanceVolumes[2] = 1.f; // center TODO: consider center scaling.
for (size_t i = 0; i < mVolumes.size(); ++i) {
//mSides表示当前channel的声场位置,mSides[i]可能是0、1、2取值,分别代表
//left位置、right位置和中间位置,在根据上面计算这3个位置对应的音量balanceVolumes
//就可以得到每个side的音量增益了
mVolumes[i] = balanceVolumes[mSides[i]];
}
}
上面代码很简单,主要是初始化好每个channel的音量数据,将参数传入到computeStereoBalance函数进行计算得到,看看是如何计算每个声道的音量:
/*
* 当blance值在[-1,0],表示左声道最大音量1.f,右声道为1.f与balance差值
* 当blance值在[0,1],表示有声道要比作声道大,同理
*/
void Balance::computeStereoBalance(float balance, float *left, float *right) const
{ //balance大于0,说明用户是要设置声道平衡在右侧
if (balance > 0.f) {
//1-balance肯定是一个小于1的数字,mCurve是一个函数,可以把[0~1]范围的值映射到[0~1]
*left = mCurve(1.f - balance);
//右边音量取1.f,保持音频本身的音量
*right = 1.f;
//小于0,道理同上
} else if (balance < 0.f) {
*left = 1.f;
*right = mCurve(1.f + balance);
//等于0的话,说明平衡点在中间,左右都保持原本的音量输出
} else {
*left = 1.f;
*right = 1.f;
}
}
上面代码也很简单,可以看我写的注释,主要根据用户设置的balance进行设置:
- 小于0,说明平衡点在左侧,左边声道肯定取值1,保持音频本身音量输出,而右声道通过1-balance在用mCurve归一化映射函数映射到小于1的值
- 大于0,说明平衡点在右侧,方法同上
这里我认为mCurve(1.f-balance)这个算法不是唯一的,我们可以根据实际测试结果进行修改,比如按照上面的配置设置后,在balance小于0的情况下,right声道完全听不到声音,可以加一个基础音量等,这个改进方法就仁者见仁智者见智了
最后,把我们的计算好的音量保存到mVolumes成员中取即可,它是一个vector类型,每个的index代表这个通道/位置的音量值,一幅图总结如下:
附加知识点—mCurve函数如何确定的
在Balance头文件中有定义,如下:
explicit Balance(
bool ramp = true, //是否用于渐变音量
std::function<float(float)> curve = [](float x) { return x * (x + 0.2f); }) //音量映射函数
: mRamp(ramp)
, mCurve(normalize(std::move(curve))) { }
以下是
f
(
x
)
=
x
(
x
+
0.2
f
)
f(x)=x(x+0.2f)
f(x)=x(x+0.2f)的函数图像:
为什么要选取这种函数呢?为啥不默认一个
y
=
x
y=x
y=x这线性函数,估计可能是音量本身不是线性的,音量增加和测量结果分贝是一种非线性关系,更像对数函数的图像,这里
f
(
x
)
=
x
(
x
+
0.2
)
这种抛物线曲线也类似的,所以就选取这种函数了
f(x)=x(x+0.2)这种抛物线曲线也类似的,所以就选取这种函数了
f(x)=x(x+0.2)这种抛物线曲线也类似的,所以就选取这种函数了
其中,normalize函数如下:
template<typename T>
static std::function<T(T)> normalize(std::function<T(T)> f) {
const T f0 = f(0);
//T(1)相当于使用构造函数,构造了一个对象T,其值为1
const T r = T(1) / (f(1) - f0); //计算得到f(1)-f(0)差值与1的大小比
if (f0 != T(0) || // must be exactly 0 at 0, since we promise g(0) == 0
//numeric_limits是c++中表达个类型数的极值库,这里表示T类型,epsilon计算机可表达
//的T类型最小正实数,比如T为float类型,则比较两个float是否相等,可以让二者相剪
//差值小于等于epsilon即可;
//进入第二个条件,也就是f0 = 0,r与1之间的差值绝对值,如果小于后者,说明函数f从入参[0,1]
//可以正常映射到[0,1],如果大于,则f函数的映射范围则有可能大于1,或者小于1,没有无限接近于1
fabs(r - T(1)) > std::numeric_limits<T>::epsilon() * 3) { // some fudge allowed on r.
//r*(fx-f0)=T(1)*(fx - f0)/(f1-f0),也就是x在[0,1]范围内比例大小,乘1,最终结果肯定在
//[0,1]之间
return [f, f0, r](T x) { return r * (f(x) - f0); };
}
// no translation required.
return f;
}
假设normalize函数命令为g(x),它能保证我们最终的映射函数2个点:
- g(0)一定等于0
- g(x<1)的情况一定也是小于1的
最终, g ( x ) g(x) g(x)的值在0~1范围内;
算法如上代码: - r可以理解为f函数在[0,1]两个点上的y值对比,假设 f ( x ) = x f(x)=x f(x)=x,那这个函数刚好满足 f ( 0 ) = 0 f(0)=0 f(0)=0且最终映射的值在y轴上也是在(0,1)
- 所以在if条件中
fabs(r - T(1)) > std::numeric_limits<T>::epsilon() * 3)
r − T ( 1 ) r-T(1) r−T(1)的绝对值就是在判断这个f函数与 y = x y=x y=x线性函数相差多大,epsilon()函数理解为T类型值得最小正整数,乘3是为了放款比较范围;这里相减后的绝对值
- 小于的话,就理解为r和T(1)理论相等,f函数无限逼近类似于 y = x y=x y=x的函数图像,
- 大于的话,就理解r和T(1)不相等,f函数远离逼近类似于
y
=
x
y=x
y=x的函数图像,可能最终映射的y值比1大,或者比1小的太多,映射不合理
理解如下这副图像:
举例y=x线性函数可能不合理,上图中y=0.5x和y=2x和代码中epsilon()也是不相符的,只是为了能清楚解释这段代码的原理,随机选择的函数
最后这行代码r * (f(x) - f0)带入r的表达式,也就是 T ( 1 ) ∗ ( f ( x ) − f ( 0 ) ) f ( 1 ) − f ( 0 ) \frac{T(1)*(f(x)-f(0))}{f(1)-f(0)} f(1)−f(0)T(1)∗(f(x)−f(0))等比例映射值而已,最终结果肯定小于1的
音量应用到音频数据中去
音量分配到音频数据中就更简单了,遍历每个音频数据,遍历每个channel,依次乘以每个channel的音量即可
void Balance::process(float *buffer, size_t frames)
{
........
if (mRamp) {
if (mRampVolumes.size() != mVolumes.size()) {
// If mRampVolumes is empty, we do not ramp in this process() but directly
// apply the existing mVolumes. We save the balance and volume state here
// and fall through to non-ramping code below. The next process() will ramp if needed.
mRampBalance = mBalance;
mRampVolumes = mVolumes;
} else if (mRampBalance != mBalance) {
if (frames > 0) {
std::vector<float> mDeltas(mVolumes.size());
//这里为啥要用1.f来作除法,估计是转换成float型,方便后续的计算
const float r = 1.f / frames;
for (size_t j = 0; j < mChannelCount; ++j) {
//通道j的开始音量mRampVolumes[j],最终音量mVolumes[j],乘r也就是除frames;
//最后mDeltas[j]就是通道j每一帧的音频增量
mDeltas[j] = (mVolumes[j] - mRampVolumes[j]) * r;
}
// ramped balance
for (size_t i = 0; i < frames; ++i) {
const float findex = i;
for (size_t j = 0; j < mChannelCount; ++j) { // better precision: delta * i
//等号后面的和在乘*buffer,开始音量+音频帧数index乘增量
*buffer++ *= mRampVolumes[j] + mDeltas[j] * findex;
}
}
}
mRampBalance = mBalance;
mRampVolumes = mVolumes;
return;
}
}
for (size_t i = 0; i < frames; ++i) {
//遍历每个通道,依次乘通道音量平衡值
for (size_t j = 0; j < mChannelCount; ++j) {
*buffer++ *= mVolumes[j];
}
}
}
上面代码很简单,主要就是ramp时渐变音量不好理解,只需要弄清ramp中的几个参数:
mRampVolumes[]:数组保存每个channel的开始音量值
mVolumes[]:数组保存每个channel最终的音量值
mDeltas[]:保存了起始音量到最终音量差值,除以音频帧数frames的值
最后*buffer++ *= mRampVolumes[j] + mDeltas[j] * findex就好理解了
扩展阅读
这里的balance把多个位置的平衡都归一化到一维上去了,那么如何改变如何扩展呢?
最常见的一个例子就是车上,四门四喇叭,设置平衡点在左前方,那么用户肯定喜欢左前方喇叭声音保持输出,其余三个喇叭适当降音;但是按照以上代码,会将后排两个喇叭都归一化到左右两个喇叭上,最终左侧喇叭声音音量一样大,右边两个喇叭一样小
思考几分钟,如何扩展呢???
扩展办法
以下是我理解的方案:
- 在setChannelMask的时候sideFromChannel数组的取值增加几个取值:0-front_left、1-front_right、2-back_left、3-back_right和4-center;
这样处理后,就可以把声道channel转换为前后左右、中间几个位置了 - 在setBalance时,传入进来的参数就要发生变化:
setBalance(float balance)
转变为:
setBalance(float balanceX, float balanceY)
- compute计算每个位置音量可以按照xy坐标组成的象限来区分:
if(balanceXb、alanceX在第一象限)
volume[front_left] = 1.f
volume[front_right] = mCurve(1.f+balanceX)
volume[back_left] = mCurve(1.f-volumeY)
volume[back_right] = mCurve(1.f-volumeY)
back_right可能还需要在调整调整,这里只是阐明一个方案而已
ok,以上就是对Balance的理解!如有不正确的地方,可以在评论区指出
顺便说个事,有个盆友要找音频开发类的工作,有合适的可以推荐以下,地点成都;