音频混音算法的研究

发布时间 2023-11-30 11:37:03作者: 阿风小子

什么是音频混音?
把多种来源的声音,整合至一个立体音轨(Stereo)或单音音轨(Mono)中,从实际的业务场景来看,比如,使用抖音上传一段本地视频的时候,可以选择添加背景音乐,合成出来的视频既有本地视频的原音,也有添加的背景音乐。

音频混音的原理
声音是由于物体的振动对周围的空气产生压力而传播的一种压力波,转成电信号后经过抽样,量化,仍然是连续平滑的波形信号,量化后的波形信号的频率与声音的频率对应,振幅与声音的音量对应,量化的语音信号的叠加等价于空气中声波的叠加,所以当采样率一致时,混音可以实现为将各对应信号的采样数据线性叠加。反应到音频数据上,也就是把同一个声道的数值进行简单的相加而问题的关键就是如何处理叠加后溢出问题。(通常的语音数据为16bit 容纳的范围是有限的 -32768 到 32767之间 所以单纯的线性叠加是有可能出现溢出问题的。 直接截断会产生噪音。 所以需要平滑过度)
所以在进行混音之前要先保证需要混合的音频 采样率、通道数、采样精度一样

我遇到的问题
我们的sdk之前已经完成了混音的工作,但是在加入了视频拼接功能后,如果拼接的是一段视频+图片,再加入背景音乐 合成之后就会出现背景音乐在播放视频的时候音量小,播放图片部分音量大。针对这个bug,我一开始是没有任何头绪的,自然也和混音算法联系不起来,我就去详细的研究了视频和图片在加上背景音乐合成的时候对音频的影响有什么不同,发现:视频具有音轨,再加上背景音乐后需要混音,而图片没有音轨,只需要加入背景音乐就行,从这一点我认为这个问题可能跟混音有关

常见的混音算法
了解了音频混音的原理后都会觉得很简单,就是把同一个声道的数值进行简单的相加,但是!!!我们存储音频数据的单位是有大小限制的,如何处理叠加后溢出问题才是混音算法的关键,并且现在已经具有的混音算法有很多,能够结合业务场景选择适合的混音算法也是关键,要求我们能理解每一种混音算法的优劣做出选择,通过查阅资料目前存在的混音算法有以下:
(使用的算法名称可能不准确,别名好多并且不保证一定对)

直接加和
平均调整权重法(平均法)
加和并箝位
归一化
自适应混音加权(衰减因子法)(改进后的归一化算法)
自动对齐算法
有人说的newlc中的一段算法
以下的代码都只是示例代码,并不能直接运行,只是展示了核心算法

1.直接加和
同一个声道的数值进行简单的相加,数据是很完整的保留下来了,但是会存在溢出的可能而且混合的路数越多,溢出的可能性越大

/**
     * @param inputAudios
     * 直接加和
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        int mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = inputAudios[0][trackOffset]+inputAudios[1][trackOffset];
            realMixAudio[trackOffset] = (short) (mixVal);

        }
        return realMixAudio;

    }


2.平均调整权重法(平均法)
将每一路的语音线性相加,再除以通道数,该方法虽然不会引入噪声,但是随着通道数成员的增多,各路语音的衰减将愈加严重。具体体现在随着通道数成员的增多,各路音量会逐步变小。

 /**
     * @param inputAudios
     * 平均调整权重法(平均法)
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        int mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = (inputAudios[0][trackOffset]+inputAudios[1][trackOffset])/2;
            realMixAudio[trackOffset] = (short) (mixVal);
        }
        return realMixAudio;
    }


3. 加和并箝(qián)位
将每一路的语音线性相加进行溢出检测,如果溢出,以最大值来替代。这样会造成声音波形的人为削峰,在破坏语音信号特性的同叫会促使噪音的产生

/**
     * @param inputAudios
     * 加和并箝位
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        //混音溢出边界
        int MAX = 32767;
        int MIN = -32768;
        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        int mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = inputAudios[0][trackOffset]+inputAudios[1][trackOffset];
            if (mixVal>MAX){
                mixVal = MAX;
            }
            if (mixVal<MIN){
                mixVal = MIN;
            }
            realMixAudio[trackOffset] = (short) (mixVal);

        }
        return realMixAudio;

    }


4. 归一化
全部乘个系数因子,使幅值归一化,但是个人认为这个归一化因子是不好确认的。

/**
     * @param inputAudios
     * 归一化
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        float f = divisor;//归一化因子
        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        float mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = (inputAudios[0][trackOffset]+inputAudios[1][trackOffset])*f;
            realMixAudio[trackOffset] = (short) (mixVal);

        }
        return realMixAudio;

    }


5. 自适应混音加权(衰减因子法)(改进后的归一化算法)
使用可变的衰减因子对语音进行衰减,该衰减因子代表了语音的权重,该衰减因子随着数据的变化而变化,当数据溢出时,则相应的使衰减因子变小,使后续的数据在衰减后处于临界值以内,没有溢出时,让衰减因子慢慢增大,使数据变化相对平滑。
算法详细解释可以参考这个链接

/**
     * @param inputAudios
     * 自适应混音加权(衰减因子法)(改进版归一化因子法)
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        float f = 1;//衰减因子 初始值为1
        //混音溢出边界
        int MAX = 32767;
        int MIN = -32768;
        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        float mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = (inputAudios[0][trackOffset]+inputAudios[1][trackOffset])*f;
            if (mixVal>MAX){
                f = MAX/mixVal;
                mixVal = MAX;
            }
            if (mixVal<MIN){
                f = MIN/mixVal;
                mixVal = MIN;
            }
            if (f < 1)
            {
                //SETPSIZE为f的变化步长,通常的取值为(1-f)/VALUE,此处取SETPSIZE 为 32   VALUE值可以取 8, 16, 32,64,128.
                f += (1 - f) / 32;
            }
            realMixAudio[trackOffset] = (short) (mixVal);

        }
        return realMixAudio;

    }


6. 自动对齐算法
考虑参与混音的多路音视频信号自身特点,以它们自身的比例作为权重,从而决定它们在合成后的输出中所占比重。

 /**
     * @param inputAudios
     * 自动对齐算法
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        float f1 = divisor1;//权重因子
        float f2 = divisor2;//权重因子

        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        float mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = inputAudios[0][trackOffset]*f1+inputAudios[1][trackOffset]*f2;
            realMixAudio[trackOffset] = (short) (mixVal);

        }
        return realMixAudio;

    }


7. 有人说的newlc中的一段算法
算法原型:
Y = A + B - (A * B / (-(2 pow(n-1) -1)))
Y = A + B - (A * B / (2 pow(n-1))
这个算法网上有很多人在引用,当我尝试把 (2 pow(n-1))替换为常量Max后进行数学推导后发现
我推不出来。。。。。。。 我只能得出的结论是,如果A=Max,B=Max,A+B-A*B/MAX=MAX
而且也有好多人提出了质疑。

/**
     * @param inputAudios
     * 有人说的newlc中的一段算法
     * @return
     */
    public static short[] mixRawAudioBytes(short[][] inputAudios) {
        int coloum = finalLength;//最终合成的音频长度
        // 音轨叠加
        short[] realMixAudio = new short[coloum];
        int mixVal;
        for (int trackOffset = 0; trackOffset < coloum; ++trackOffset) {
            mixVal = 0;
            if (inputAudios[0][trackOffset] < 0 && inputAudios[1][trackOffset] < 0) {
                mixVal = inputAudios[0][trackOffset] + inputAudios[1][trackOffset] - (inputAudios[0][trackOffset] * inputAudios[1][trackOffset] / MIN);
            } else {
                mixVal = inputAudios[0][trackOffset] + inputAudios[1][trackOffset] - (inputAudios[0][trackOffset] * inputAudios[1][trackOffset] / MAX);
            }
            realMixAudio[trackOffset] = (short) (mixVal);

        }

        return realMixAudio;

    }



以上就是我知道的混音算法的例子
回到我遇到的问题本身,研究代码发现,项目中现在用的混音算法是平均调整权重法(平均法),那这样在混音的时候就会导致背景音乐的音量被消减了,而图片不需要混音,背景音乐不需要被削减,就出现了一段声音大一段声音小的问题了,那解决这个问题就是要在保证不溢出的情况下保留原幅,业务场景只有两路音轨,所以其实可以选择的有很多,不同的业务场景需要采用不同的混音算法

题外话: 自从实习以来就没有再写过播客了,工作真的好累好累啊,加上我们主要做的是音视频相关的工作,又很难,想写清楚一个问题真的是一件不容易的事情,所以以后的文章内容大部分都是我在工作中遇到的实际问题。希望自己能坚持记录