音频Balance源码总结

音频Balance源码总结

何为音频Balance?

顾名思义,Balance及平衡,平衡也就是涉及多方,音频左右甚至四通道,调节所有通道的音量比,使用户在空间内听到各个通道的音频大小不一,好似置身于真实环境中;

博主分析的Balance源码在

c 复制代码
./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之间必须要先调用

c++ 复制代码
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处理方式
c++ 复制代码
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;
    }
}

以上函数主要做了以下几件事情:

  1. 从ChannelMask中获取通道数,并根据通道数resize重置mVolumes(vector类型)大小为channelCount,因为balance就是根据通道来进行加减乘除的
  2. 如果channelMask是AUDIO_CHANNEL_REPRESENTATION_INDEX格式,也就是index方式来表示通道数据,无需用mSide位置来表示
  3. setBalance为每个声道设置对应的音量,后面展开
AUDIO_CHANNEL_REPRESENTATION_POSITION处理方式

以下这段代码也是在setChannelMask方法中的,是当channelMask为AUDIO_CHANNEL_REPRESENTATION_POSITION的处理方式

c++ 复制代码
    //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

c++ 复制代码
//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函数进行计算得到,看看是如何计算每个声道的音量:

c++ 复制代码
/*
* 当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进行设置:

  1. 小于0,说明平衡点在左侧,左边声道肯定取值1,保持音频本身音量输出,而右声道通过1-balance在用mCurve归一化映射函数映射到小于1的值
  2. 大于0,说明平衡点在右侧,方法同上

这里我认为mCurve(1.f-balance)这个算法不是唯一的,我们可以根据实际测试结果进行修改,比如按照上面的配置设置后,在balance小于0的情况下,right声道完全听不到声音,可以加一个基础音量等,这个改进方法就仁者见仁智者见智了

最后,把我们的计算好的音量保存到mVolumes成员中取即可,它是一个vector类型,每个的index代表这个通道/位置的音量值,一幅图总结如下:

附加知识点---mCurve函数如何确定的

在Balance头文件中有定义,如下:

c++ 复制代码
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函数如下:

c++ 复制代码
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个点:

  1. g(0)一定等于0
  2. g(x<1)的情况一定也是小于1的
    最终, g ( x ) g(x) g(x)的值在0~1范围内;
    算法如上代码:
  3. 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)
  4. 所以在if条件中
c++ 复制代码
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的音量即可

c++ 复制代码
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把多个位置的平衡都归一化到一维上去了,那么如何改变如何扩展呢?

最常见的一个例子就是车上,四门四喇叭,设置平衡点在左前方,那么用户肯定喜欢左前方喇叭声音保持输出,其余三个喇叭适当降音;但是按照以上代码,会将后排两个喇叭都归一化到左右两个喇叭上,最终左侧喇叭声音音量一样大,右边两个喇叭一样小

思考几分钟,如何扩展呢???

扩展办法

以下是我理解的方案:

  1. 在setChannelMask的时候sideFromChannel数组的取值增加几个取值:0-front_left、1-front_right、2-back_left、3-back_right和4-center;
    这样处理后,就可以把声道channel转换为前后左右、中间几个位置了
  2. 在setBalance时,传入进来的参数就要发生变化:
c++ 复制代码
setBalance(float balance)
转变为:
setBalance(float balanceX, float balanceY)
  1. 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的理解!如有不正确的地方,可以在评论区指出

顺便说个事,有个盆友要找音频开发类的工作,有合适的可以推荐以下,地点成都;

相关推荐
人工智能培训咨询叶梓13 分钟前
Lumière:开创性的视频生成模型及其应用
人工智能·深度学习·机器学习·语言模型·自然语言处理·音视频·多模态
玩电脑的辣条哥1 小时前
如何快速去除视频里面的水印字幕等信息?(内附工具)
ai·音视频·短视频
Susu_afmx1 小时前
音乐伴奏提取?唱歌剪辑好用的音频人声分离软件!提取步骤很简单!
深度学习·新媒体运营·电脑·音视频·音频
星星月亮03 小时前
iOS Swift5 视频播放 能播放各种编码格式的视频的第三方库
ios·音视频
爱玩游戏的jason5 小时前
如何获取音频伴奏
音视频
写代码的小黑14 小时前
视频添加字幕
python·音视频
Leventure_轩先生14 小时前
[ALSA]从零开始,使用ALSA驱动播放一个音频
开发语言·c++·音视频
安步当歌18 小时前
【FFmpeg】av_read_frame函数
c语言·c++·ffmpeg·视频编解码·video-codec
智光工作室18 小时前
AudioLM音频生成模型
音视频·audiolm
zhqh10018 小时前
ffmpeg + opencv 把摄像头画面保存为mp4文件(Ubuntu24.04)
人工智能·opencv·ffmpeg