Android中一種效果奇好的混音方法詳解
初識(shí)音頻
從初中物理上我們就學(xué)到,聲音是一種波。計(jì)算機(jī)只能處理離散的信號(hào),通過(guò)收集足夠多的離散的信號(hào),來(lái)不斷逼近波形,這個(gè)過(guò)程我們叫做采樣。怎么樣才能更好的還原聲音信息呢?這里很自然引出兩個(gè)概念了。

采樣頻率(Sample Rate):每秒采集聲音的數(shù)量,它用赫茲(Hz)來(lái)表示。
采樣率越高越靠近原聲音的波形,常見(jiàn)的采樣率有以下幾種:
- 8khz:電話(huà)等使用,對(duì)于記錄人聲已經(jīng)足夠使用。
- 22.05khz:廣播使用頻率。
- 44.1kb:音頻CD。
- 48khz:DVD、數(shù)字電視中使用。
- 96khz-192khz:DVD-Audio、藍(lán)光高清等使用。
采樣精度(Bit Depth): 它表示每次采樣的精度,位數(shù)越多,能記錄的范圍就越大。
采樣精度常用范圍為8bit-32bit,而CD中一般都使用16bit。
把聲音記錄下來(lái)之后,通過(guò)喇叭的震動(dòng)把波再還給空氣傳到你的耳朵就完成了這個(gè)完美的循環(huán)了。但是富有創(chuàng)造力的人類(lèi)不會(huì)限制于此就結(jié)束了,很快人們發(fā)現(xiàn),當(dāng)把不同的聲音傳遞到不同的喇叭的時(shí)候,竟然會(huì)驚奇地讓聲音變得有空間感了,即時(shí)是同一個(gè)聲音,也比單個(gè)通道能獲得更好的體驗(yàn),于是就出現(xiàn)了什么立體聲,5.1 環(huán)繞等看起來(lái)很高大上的東西。所以,音頻又多了一個(gè)東西:
聲音通道(Channel): 你知道每個(gè)通道存儲(chǔ)的聲音會(huì)從其中的一個(gè)喇叭出來(lái)就好了,不過(guò)可以通過(guò)算法的模擬來(lái)讓沒(méi)有那么多喇叭也能出來(lái)類(lèi)似的效果。
有了聲音通道,樂(lè)隊(duì)在錄音的時(shí)候就可以每個(gè)人插一條音軌了,然后每一個(gè)聲音可以寫(xiě)到不同的通道里面,當(dāng)然,實(shí)際錄音當(dāng)然都是后期混音而成的。下面介紹的其中一個(gè)混音算法會(huì)用到聲音通道這個(gè)特性。
最后再介紹一個(gè)大家經(jīng)常看到的概念:
比特率(bps [bits per second]): 其實(shí)看單位就很容易知道它要表達(dá)的意思了,就是每秒鐘要播放多少 bit 的數(shù)據(jù)。公式一目了然:
比特率 = 采樣率 × 采樣深度 × 通道。
比如 采樣率 = 44100,采樣深度 = 16,通道 = 2 的音頻的的比特率就是 44100 16 2 = 1411200 bps。
一般來(lái)說(shuō),比特率越高,音頻質(zhì)量越好。要注意一些比特率的換算不是 1024 作為一個(gè)級(jí)別換算的哈。
1,000 bps = 【1 kbps】 = 1,000 bit/s
1,000,000 bps = 【1 Mbps】 = 1,000,000 bit/s
1,000,000,000 bps = 【1 Gbps】 = 1,000,000,000 bit/s
音頻在計(jì)算機(jī)中的表示
我們來(lái)看一下真實(shí)音頻在計(jì)算機(jī)中究竟是怎樣的表示狀態(tài),這里指的是原始的數(shù)據(jù)表示,而非編碼(Mp3,Acc等)后的表示,平時(shí)我們看到的.wav后綴的音頻,把前面 44 個(gè)字節(jié)用于記錄采樣率、通道等的頭部信息去掉后就是就是原始的音頻數(shù)據(jù)了。

在理解了上面的概念之后,我們?cè)賮?lái)看這張圖。對(duì)于文件頭部信息我們就不詳細(xì)介紹了,不影響我們理解介紹的混音處理方式,需要了解的可以點(diǎn)擊這里。
我們抽取其中的一個(gè)采樣來(lái)看,這里我加多了一個(gè)通道,便于大家理解通道的存儲(chǔ)位置。

不難理解,這個(gè)采樣中有三個(gè)通道,每通道采樣精度是 16 比特。每個(gè)采樣值的排序是 Little-Endian 低位在前的方式,比如通道 1 的采樣值就是 AB03, 每個(gè)采樣值的大小表示的是幅度信息。
混音的原理
音頻混音的原理: 空氣中聲波的疊加等價(jià)于量化的語(yǔ)音信號(hào)的疊加。

這句話(huà)可能有點(diǎn)拗口,我們從程序員的角度去觀(guān)察就不難理解了。下圖是兩條音軌的數(shù)據(jù),將每個(gè)通道的值做線(xiàn)性疊加后的值就是混音的結(jié)果了。比如音軌A和音軌B的疊加,A.1 表示 A 音軌的 1 通道的值 AB03 , B.1 表示 B 音軌的 1 通道的值 1122 , 結(jié)果是 bc25,然后按照低位在前的方式排列,在合成音軌中就是 25bc,這里的表示都是 16 進(jìn)制的。

直接加起來(lái)就可以了?事情如果這么簡(jiǎn)單就好了。音頻設(shè)備支持的采樣精度肯定都是有限的,一般為 8 位或者 16 位,大一些的為 32 位。在音軌數(shù)據(jù)疊加的過(guò)程中,肯定會(huì)導(dǎo)致溢出的問(wèn)題。為了解決這個(gè)問(wèn)題,人們找了不少的辦法。這里我主要介紹幾種我用過(guò)的,并給出相關(guān)代碼實(shí)現(xiàn)和最終的混音效果對(duì)比結(jié)果。
線(xiàn)性疊加平均
這種辦法的原理非常簡(jiǎn)單粗暴,也不會(huì)引入噪音。原理就是把不同音軌的通道值疊加之后取平均值,這樣就不會(huì)有溢出的問(wèn)題了。但是會(huì)帶來(lái)的后果就是某一路或幾路音量特別小那么整個(gè)混音結(jié)果的音量會(huì)被拉低。
以下的的單路音軌的音頻參數(shù)我們假定為采樣頻率一致,通道數(shù)一致,通道采樣精度統(tǒng)一為 16 位。
其中參數(shù) bMulRoadAudios 的一維表示的是音軌數(shù),二維表示該音軌的音頻數(shù)據(jù)。
Java 代碼實(shí)現(xiàn):
@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;
//不同軌道長(zhǎng)度要一致,不夠要補(bǔ)齊
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;
}
自適應(yīng)混音
參與混音的多路音頻信號(hào)自身的特點(diǎn),以它們自身的比例作為權(quán)重,從而決定它們?cè)诤铣珊蟮妮敵鲋兴嫉谋戎亍>唧w的原理可以參考這篇論文:快速實(shí)時(shí)自適應(yīng)混音方案研究。這種方法對(duì)于音軌路數(shù)比較多的情況應(yīng)該會(huì)比上面的平均法要好,但是可能會(huì)引入噪音。
Java 代碼實(shí)現(xiàn):
@Override
public byte[] mixRawAudioBytes(byte[][] bMulRoadAudios) {
//簡(jiǎn)化檢查代碼
/**
* 精度為 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;
}
多通道混音
在實(shí)際開(kāi)發(fā)中,我發(fā)現(xiàn)上面的兩種方法都不能達(dá)到滿(mǎn)意的效果。一方面是和音樂(lè)相關(guān),對(duì)音頻質(zhì)量要求比較高;另外一方面是通過(guò)手機(jī)錄音,效果肯定不會(huì)太好。不知道從哪里冒出來(lái)的靈感,為什么不試著把不同的音軌數(shù)據(jù)塞到不同的通道上,讓聲音從不同的喇叭上同時(shí)發(fā)出,這樣也可以達(dá)到混音的效果??!而且不會(huì)有音頻數(shù)據(jù)損失的問(wèn)題,能很完美地呈現(xiàn)原來(lái)的聲音。
于是我開(kāi)始查了一下 Android 對(duì)多通道的支持情況,對(duì)應(yīng)代碼可以在android.media.AudioFormat中查看,結(jié)果如下:
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 個(gè)通道,對(duì)于我的情況來(lái)說(shuō)是完全夠用了。我們的耳機(jī)一般只有左右聲道,那些更多通道的支持是 Android 系統(tǒng)內(nèi)部通過(guò)軟件算法模擬實(shí)現(xiàn)的,至于具體如何實(shí)現(xiàn)的,我也沒(méi)有深入了解,在這里我們知道這回事就行了。我們平時(shí)所熟知的立體聲,5.1 環(huán)繞等就是上面那些通道的組合。
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);
知道原理之后,實(shí)現(xiàn)起來(lái)非常簡(jiǎn)單,下面是具體的代碼:
@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;
}
結(jié)果比較
線(xiàn)性疊加平均法雖然看起來(lái)很簡(jiǎn)單,但是在音軌數(shù)量比較少的時(shí)候取得的效果可能會(huì)比復(fù)雜的自適應(yīng)混音法要出色。
自適應(yīng)混音法比較合適音軌數(shù)量比較多的情況,但是可能會(huì)引入一些噪音。
多通道混音雖然看起來(lái)很完美,但是產(chǎn)生的文件大小是數(shù)倍于其他的處理方法。
沒(méi)有銀彈,還是要根據(jù)自己的應(yīng)用場(chǎng)景來(lái)選擇,多試一下。
下面是我錄的兩路音軌:
音軌一:
音軌二:
線(xiàn)性疊加平均法:
自適應(yīng)混音法:
多通道混音:
采樣頻率、采樣精度和通道數(shù)不同的情況如何處理?
不同采樣頻率需要算法進(jìn)行重新采樣處理,讓所有音軌在同一采樣率下進(jìn)行混音,這個(gè)比較復(fù)雜,等有機(jī)會(huì)再寫(xiě)篇文章介紹。
采樣精度不同比較好處理,向上取精度較高的作為基準(zhǔn)即可,高位補(bǔ)0;如果是需要取向下精度作為基準(zhǔn)的,那么就要把最大通道值和基準(zhǔn)最大值取個(gè)倍數(shù),把數(shù)值都降到最大基準(zhǔn)數(shù)以下,然后把低位移除。
通道數(shù)不同的情況也和精度不同的情況相似處理。
參考資料
多媒體會(huì)議中的快速實(shí)時(shí)自適應(yīng)混音方案研究
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Android自定義控件實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了Android自定義控件實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android控件之ImageView用法實(shí)例分析
這篇文章主要介紹了Android控件之ImageView用法,以實(shí)例形式較為詳細(xì)的分析了ImageView控件用于顯示圖片的使用方法,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09
Android開(kāi)發(fā)方式之Java+html+javascript混合開(kāi)發(fā)
這篇文章主要為大家詳細(xì)介紹了Android開(kāi)發(fā)方式的其中一種Java+html+javascript混合開(kāi)發(fā),感興趣的小伙伴們可以參考一下2016-06-06
Android scrollview實(shí)現(xiàn)底部繼續(xù)拖動(dòng)查看圖文詳情
這篇文章主要為大家詳細(xì)介紹了Android scrollview實(shí)現(xiàn)底部繼續(xù)拖動(dòng)查看圖文詳情,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-02-02
Android基于widget組件實(shí)現(xiàn)物體移動(dòng)/控件拖動(dòng)功能示例
這篇文章主要介紹了Android基于widget組件實(shí)現(xiàn)物體移動(dòng)/控件拖動(dòng)功能,結(jié)合實(shí)例形式分析了widget組件在桌面應(yīng)用中的事件響應(yīng)與屬性動(dòng)態(tài)操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2016-10-10
Android通話(huà)記錄備份實(shí)現(xiàn)代碼
其實(shí)就是解析文件,存入數(shù)據(jù)庫(kù);或者查詢(xún)數(shù)據(jù)庫(kù),存入文件而已,特分享下,方便需要的朋友2013-05-05
基于Flutter制作一個(gè)長(zhǎng)按展示操作項(xiàng)面板的桌面圖標(biāo)
Flutter是一種強(qiáng)大的跨平臺(tái)移動(dòng)應(yīng)用程序框架,它能夠幫助開(kāi)發(fā)者輕松地創(chuàng)建漂亮、快速、高效的應(yīng)用程序,本文的主題是如何在Flutter中制作一個(gè)長(zhǎng)按展示操作項(xiàng)面板的桌面圖標(biāo),在某些場(chǎng)景下,這個(gè)功能會(huì)讓?xiě)?yīng)用程序更加便利和易用2023-06-06
android上一個(gè)可追蹤代碼具體到函數(shù)某行的日志類(lèi)
追蹤代碼到函數(shù)具體某行,這樣的功能,是每一個(gè)程序員都希望會(huì)有的,因?yàn)樗梢詭椭覀冏粉櫟侥承写a的錯(cuò)誤,接下來(lái)介紹下android上一個(gè)可追蹤代碼到函數(shù)具體某行的日志類(lèi),希望對(duì)開(kāi)發(fā)者有所幫助2012-12-12

