如果對音訊的一些基礎知識還不是很瞭解的建議先去閱讀一下上一篇文章:寫給小白的音訊認識基礎 。
混音的原理
音訊混音的原理: 空氣中聲波的疊加等價於量化的語音訊號的疊加。
這句話可能有點拗口,我們從程式設計師的角度去觀察就不難理解了。下圖是兩條音軌的資料,將每個通道的值做線性疊加後的值就是混音的結果了。比如音軌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;
}
複製程式碼
結果比較
線性疊加平均法雖然看起來很簡單,但是在音軌數量比較少的時候取得的效果可能會比複雜的自適應混音法要出色。
自適應混音法比較合適音軌數量比較多的情況,但是可能會引入一些噪音。
多通道混音雖然看起來很完美,但是產生的檔案大小是數倍於其他的處理方法。
沒有銀彈,還是要根據自己的應用場景來選擇,多試一下。
下面是我錄的兩路音軌:
- 音軌一:
- 音軌二:
- 線性疊加平均法:
- 自適應混音法:
- 多通道混音:
取樣頻率、取樣精度和通道數不同的情況如何處理?
不同取樣頻率需要演算法進行重新取樣處理,讓所有音軌在同一取樣率下進行混音,這個比較複雜,等有機會再寫篇文章介紹。
取樣精度不同比較好處理,向上取精度較高的作為基準即可,高位補0;如果是需要取向下精度作為基準的,那麼就要把最大通道值和基準最大值取個倍數,把數值都降到最大基準數以下,然後把低位移除。
通道數不同的情況也和精度不同的情況相似處理。
參考資料
技術交流群:70948803,大部分時間群裡都是安靜的,只交流技術相關,很少發言,不歡迎廣告噴子。
不玩音樂的看到這裡可以關閉了。
色彩濃重的廣告時間:
如果你有玩音樂,我做了一個音樂學習和記錄的輔助工具。剛在 Google Play 釋出,可以直接點選這裡下載:下載聲音筆記+。我平時會用它來作即興練習和合奏練習。
以下是免費的: