若是对音频的一些基础知识还不是很了解的建议先去阅读一下上一篇文章:写给小白的音频认识基础 。java
音频混音的原理: 空气中声波的叠加等价于量化的语音信号的叠加。android
这句话可能有点拗口,咱们从程序员的角度去观察就不难理解了。下图是两条音轨的数据,将每一个通道的值作线性叠加后的值就是混音的结果了。好比音轨A和音轨B的叠加,A.1
表示 A 音轨的 1 通道的值 AB03
, B.1
表示 B 音轨的 1 通道的值 1122
, 结果是 bc25
,而后按照低位在前的方式排列,在合成音轨中就是 25bc
,这里的表示都是 16 进制的。ios
直接加起来就能够了?事情若是这么简单就行了。音频设备支持的采样精度确定都是有限的,通常为 8 位或者 16 位,大一些的为 32 位。在音轨数据叠加的过程当中,确定会致使溢出的问题。为了解决这个问题,人们找了很多的办法。这里我主要介绍几种我用过的,并给出相关代码实现和最终的混音效果对比结果。程序员
这种办法的原理很是简单粗暴,也不会引入噪音。原理就是把不一样音轨的通道值叠加以后取平均值,这样就不会有溢出的问题了。可是会带来的后果就是某一路或几路音量特别小那么整个混音结果的音量会被拉低。算法
如下的的单路音轨的音频参数咱们假定为采样频率一致,通道数一致,通道采样精度统一为 16 位。app
其中参数 bMulRoadAudios
的一维表示的是音轨数,二维表示该音轨的音频数据。ide
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;
}
复制代码
线性叠加平均法虽然看起来很简单,可是在音轨数量比较少的时候取得的效果可能会比复杂的自适应混音法要出色。
自适应混音法比较合适音轨数量比较多的状况,可是可能会引入一些噪音。
多通道混音虽然看起来很完美,可是产生的文件大小是数倍于其余的处理方法。
没有银弹,仍是要根据本身的应用场景来选择,多试一下。
下面是我录的两路音轨:
不一样采样频率须要算法进行从新采样处理,让全部音轨在同一采样率下进行混音,这个比较复杂,等有机会再写篇文章介绍。
采样精度不一样比较好处理,向上取精度较高的做为基准便可,高位补0;若是是须要取向下精度做为基准的,那么就要把最大通道值和基准最大值取个倍数,把数值都降到最大基准数如下,而后把低位移除。
通道数不一样的状况也和精度不一样的状况类似处理。
技术交流群:70948803,大部分时间群里都是安静的,只交流技术相关,不多发言,不欢迎广告喷子。
不玩音乐的看到这里能够关闭了。
色彩浓重的广告时间:
若是你有玩音乐,我作了一个音乐学习和记录的辅助工具。刚在 Google Play 发布,能够直接点击这里下载:下载声音笔记+。我平时会用它来做即兴练习和合奏练习。
如下是免费的: