若是對音頻的一些基礎知識還不是很瞭解的建議先去閱讀一下上一篇文章:寫給小白的音頻認識基礎 。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 發佈,能夠直接點擊這裏下載:下載聲音筆記+。我平時會用它來做即興練習和合奏練習。
如下是免費的: