文档章节

Android上一种效果奇好的混音方法介绍

叶大侠
 叶大侠
发布于 03/11 02:22
字数 1965
阅读 237
收藏 9

如果对音频的一些基础知识还不是很了解的建议先去阅读一下上一篇文章:写给小白的音频认识基础

混音的原理

音频混音的原理: 空气中声波的叠加等价于量化的语音信号的叠加。

混音原理图

这句话可能有点拗口,我们从程序员的角度去观察就不难理解了。下图是两条音轨的数据,将每个通道的值做线性叠加后的值就是混音的结果了。比如音轨A和音轨B的叠加,A.1 表示 A 音轨的 1 通道的值 AB03 , B.1 表示 B 音轨的 1 通道的值 1122 , 结果是 bc25,然后按照低位在前的方式排列,在合成音轨中就是 25bc,这里的表示都是 16 进制的。

音轨合成图

直接加起来就可以了?事情如果这么简单就好了。音频设备支持的采样精度肯定都是有限的,一般为 8 位或者 16 位,大一些的为 32 位。在音轨数据叠加的过程中,肯定会导致溢出的问题。为了解决这个问题,人们找了不少的办法。这里我主要介绍几种我用过的,并给出相关代码实现和最终的混音效果对比结果。

线性叠加平均

这种办法的原理非常简单粗暴,也不会引入噪音。原理就是把不同音轨的通道值叠加之后取平均值,这样就不会有溢出的问题了。但是会带来的后果就是某一路或几路音量特别小那么整个混音结果的音量会被拉低。

以下的的单路音轨的音频参数我们假定为采样频率一致,通道数一致,通道采样精度统一为 16 位。

其中参数 bMulRoadAudios 的一维表示的是音轨数,二维表示该音轨的音频数据。

Java 代码实现:

@Override
        public byte[] mixRawAudioBytes(byte[][] bMulRoadAudios) {

            if (bMulRoadAudios == null || bMulRoadAudios.length == 0)
                return null;

            byte[] realMixAudio = bMulRoadAudios[0];
            if(realMixAudio == null){
                return null;
            }

            final int row = bMulRoadAudios.length;

            //单路音轨
            if (bMulRoadAudios.length == 1)
                return realMixAudio;

            //不同轨道长度要一致,不够要补齐

            for (int rw = 0; rw < bMulRoadAudios.length; ++rw) {
                if (bMulRoadAudios[rw] == null || bMulRoadAudios[rw].length != realMixAudio.length) {
                    return null;
                }
            }

            /**
             * 精度为 16位
             */
            int col = realMixAudio.length / 2;
            short[][] sMulRoadAudios = new short[row][col];

            for (int r = 0; r < row; ++r) {
                for (int c = 0; c < col; ++c) {
                    sMulRoadAudios[r][c] = (short) ((bMulRoadAudios[r][c * 2] & 0xff) | (bMulRoadAudios[r][c * 2 + 1] & 0xff) << 8);
                }
            }

            short[] sMixAudio = new short[col];
            int mixVal;
            int sr = 0;
            for (int sc = 0; sc < col; ++sc) {
                mixVal = 0;
                sr = 0;
                for (; sr < row; ++sr) {
                    mixVal += sMulRoadAudios[sr][sc];
                }
                sMixAudio[sc] = (short) (mixVal / row);
            }

            for (sr = 0; sr < col; ++sr) {
                realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
                realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
            }

            return realMixAudio;
        }

自适应混音

参与混音的多路音频信号自身的特点,以它们自身的比例作为权重,从而决定它们在合成后的输出中所占的比重。具体的原理可以参考这篇论文:快速实时自适应混音方案研究。这种方法对于音轨路数比较多的情况应该会比上面的平均法要好,但是可能会引入噪音。

Java 代码实现:

       @Override
        public byte[] mixRawAudioBytes(byte[][] bMulRoadAudios) {
            //简化检查代码

            /**
             * 精度为 16位
             */
            int col = realMixAudio.length / 2;
            short[][] sMulRoadAudios = new short[row][col];

            for (int r = 0; r < row; ++r) {
                for (int c = 0; c < col; ++c) {
                    sMulRoadAudios[r][c] = (short) ((bMulRoadAudios[r][c * 2] & 0xff) | (bMulRoadAudios[r][c * 2 + 1] & 0xff) << 8);
                }
            }

            short[] sMixAudio = new short[col];
            int sr = 0;

            double wValue;
            double absSumVal;

            for (int sc = 0; sc < col; ++sc) {
                sr = 0;

                wValue = 0;
                absSumVal = 0;

                for (; sr < row; ++sr) {
                    wValue += Math.pow(sMulRoadAudios[sr][sc], 2) * Math.signum(sMulRoadAudios[sr][sc]);
                    absSumVal += Math.abs(sMulRoadAudios[sr][sc]);
                }

                sMixAudio[sc] = absSumVal == 0 ? 0 : (short) (wValue / absSumVal);
            }

            for (sr = 0; sr < col; ++sr) {
                realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
                realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
            }

            return realMixAudio;
        }

多通道混音

在实际开发中,我发现上面的两种方法都不能达到满意的效果。一方面是和音乐相关,对音频质量要求比较高;另外一方面是通过手机录音,效果肯定不会太好。不知道从哪里冒出来的灵感,为什么不试着把不同的音轨数据塞到不同的通道上,让声音从不同的喇叭上同时发出,这样也可以达到混音的效果啊!而且不会有音频数据损失的问题,能很完美地呈现原来的声音。

于是我开始查了一下 Android 对多通道的支持情况,对应代码可以在android.media.AudioFormat中查看,结果如下:

public static final int CHANNEL_OUT_FRONT_LEFT = 0x4;
public static final int CHANNEL_OUT_FRONT_RIGHT = 0x8;
public static final int CHANNEL_OUT_FRONT_CENTER = 0x10;
public static final int CHANNEL_OUT_LOW_FREQUENCY = 0x20;
public static final int CHANNEL_OUT_BACK_LEFT = 0x40;
public static final int CHANNEL_OUT_BACK_RIGHT = 0x80;
public static final int CHANNEL_OUT_FRONT_LEFT_OF_CENTER = 0x100;
public static final int CHANNEL_OUT_FRONT_RIGHT_OF_CENTER = 0x200;
public static final int CHANNEL_OUT_BACK_CENTER = 0x400;
public static final int CHANNEL_OUT_SIDE_LEFT =         0x800;
public static final int CHANNEL_OUT_SIDE_RIGHT =       0x1000;

一共支持 10 个通道,对于我的情况来说是完全够用了。我们的耳机一般只有左右声道,那些更多通道的支持是 Android 系统内部通过软件算法模拟实现的,至于具体如何实现的,我也没有深入了解,在这里我们知道这回事就行了。我们平时所熟知的立体声,5.1 环绕等就是上面那些通道的组合。

 int CHANNEL_OUT_MONO = CHANNEL_OUT_FRONT_LEFT;

 int CHANNEL_OUT_STEREO = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT);

 int CHANNEL_OUT_5POINT1 = (CHANNEL_OUT_FRONT_LEFT | CHANNEL_OUT_FRONT_RIGHT |
            CHANNEL_OUT_FRONT_CENTER | CHANNEL_OUT_LOW_FREQUENCY | CHANNEL_OUT_BACK_LEFT | CHANNEL_OUT_BACK_RIGHT);

知道原理之后,实现起来非常简单,下面是具体的代码:

        @Override
        public byte[] mixRawAudioBytes(byte[][] bMulRoadAudios) {

            int roadLen = bMulRoadAudios.length;

            //单路音轨
            if (roadLen == 1)
                return bMulRoadAudios[0];

            int maxRoadByteLen = 0;

            for(byte[] audioData : bMulRoadAudios){
                if(maxRoadByteLen < audioData.length){
                    maxRoadByteLen = audioData.length;
                }
            }

            byte[] resultMixData = new byte[maxRoadByteLen * roadLen];

            for(int i = 0; i != maxRoadByteLen; i = i + 2){
                for(int r = 0; r != roadLen; r++){
                    resultMixData[i * roadLen + 2 * r] = bMulRoadAudios[r][i];
                    resultMixData[i * roadLen + 2 * r + 1] = bMulRoadAudios[r][i+1];
                }
            }
            return resultMixData;
        }

结果比较

线性叠加平均法虽然看起来很简单,但是在音轨数量比较少的时候取得的效果可能会比复杂的自适应混音法要出色。

自适应混音法比较合适音轨数量比较多的情况,但是可能会引入一些噪音。

多通道混音虽然看起来很完美,但是产生的文件大小是数倍于其他的处理方法。

没有银弹,还是要根据自己的应用场景来选择,多试一下。

下面是我录的两路音轨:

由于这里看不到音频,你可以转到这里去体验一下。

  • 音轨一:

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/feb3bfa6-0984-4fa6-a406-228bf5ecb6ad.wav" type="audio/wav" /> </audio>

  • 音轨二:

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/8ac76e2a-792d-4716-907e-63fe602b54f1.wav" /> </audio>

  • 线性叠加平均法:

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/Average-audio.wav" /> </audio>

  • 自适应混音法:

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/AutoAlign-audio.wav" /> </audio>

  • 多通道混音:

<audio controls="controls" height="40" width="100"> <source src="http://ohb4y25jk.bkt.clouddn.com/ChannelAlign-audio.wav" /> </audio>

采样频率、采样精度和通道数不同的情况如何处理?

不同采样频率需要算法进行重新采样处理,让所有音轨在同一采样率下进行混音,这个比较复杂,等有机会再写篇文章介绍。

采样精度不同比较好处理,向上取精度较高的作为基准即可,高位补0;如果是需要取向下精度作为基准的,那么就要把最大通道值和基准最大值取个倍数,把数值都降到最大基准数以下,然后把低位移除。

通道数不同的情况也和精度不同的情况相似处理。

参考资料

  1. 多媒体会议中的快速实时自适应混音方案研究

© 著作权归作者所有

共有 人打赏支持
叶大侠

叶大侠

粉丝 58
博文 44
码字总数 67312
作品 5
广州
程序员
私信 提问
Audio Evolution DAW for Android 发布

Audio Evolution Mobile for Android 发布!这是一款融合多轨音频与非线性编辑、混音、实时效果等功能的应用的 Android 应用,可在 Android 手机和平板上使用(需要满足配置要求,分辨率 80...

黄金小瓜
2012/03/27
2.8K
5
短视频app开发——Android端实用短视频录制方案

继抖音、快手、微视等一众短视频豪强并起以来,2018年的短视频市场可谓一片火热,而国内很多短视频平台运营商也开始纷纷布局海外短视频市场。面对眼前的场景,短视频app开发也逐渐引发了创投...

q3557873521
11/16
0
0
Android实现自定义铃音

不知道大家知道不知道,Android手机设置铃音这个功能跟其他手机平台不太一样,其他平台设置的时候一般可以浏览本地铃音库或者曲库,然后选中即可设置。而Android中不行,它只有手机出厂时内置...

晨曦之光
2012/03/05
1K
0
基于Google动态化方案的组件化演进

  国内Android动态化方案已经蓬勃发展数年之久,在React Natvie、Flutter这些跨平台方案未出现之前,类似Atlas、Replugin、DLA等Android动态化方案在业界独领风骚。在国内动态化方案也分为...

Android群英传
07/19
0
0
Android 5.1 Phone DTMF流程分析

写在前面的话 本文主要分析Android DTMF的流程,研究的代码是Android 5.1的,以CDMA为例,GSM同理。 在手机中,常用的DTMF场景是使用手机拨打一些服务台电话,比如客服热线10086、10000之类;...

linyongan
2015/08/11
0
0

没有更多内容

加载失败,请刷新页面

加载更多

《大漠烟尘》读书笔记及读后感文章3700字

《大漠烟尘》读书笔记及读后感文章3700字: 在这个浮躁的社会里,你有多久没有好好读完一本书了? 我们总觉得自己和别人不一样,所以当看到别人身上的问题时,很少有“反求诸己”,反思自己。...

原创小博客
24分钟前
1
0
大数据教程(9.5)用MR实现sql中的jion逻辑

上一篇博客讲解了使用jar -jar的方式来运行提交MR程序,以及通过修改YarnRunner的源码来实现MR的windows开发环境提交到集群的方式。本篇博主将分享sql中常见的join操作。 一、需求 订单数据表...

em_aaron
32分钟前
1
0
十万个为什么之什么是resultful规范

起源 越来越多的人开始意识到,网站即软件,而且是一种新型的软件。这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点...

尾生
38分钟前
1
0
Terraform配置文件(Terraform configuration)

Terraform配置文件 翻译自Terraform Configuration Terraform用文本文件来描述设备、设置变量。这些文件被称为Terraform配置文件,以.tf结尾。这一部分将讲述Terraform配置文件的加载与格式。...

buddie
今天
2
0
exportfs命令, vsftp搭建ftp服务

exportfs命令 当修改/etc/exports文件后,更改的内容是不会立即生效的。如果重启nfs服务,会导致客户端重启期间的请求是挂起等待的,可以把客户端的挂载umount进行卸载后,再重启nfs服务,但...

野雪球
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部