本文已經(jīng)投稿其他地方外莲,本處拒絕轉(zhuǎn)載猪半!
1. 背景
提到混音很多想到的是玩音樂的人用一些專業(yè)的設(shè)備做一些很炫但是看不懂的事情...
在移動直播場景中, 混音可以是用于主播一邊播放音樂, 一邊直播, 也就是一般說的背景音樂功能. 或者是直播連麥中, 將輔播的聲音和主播的聲音混合. 還有在畫中畫等功能中將,視頻的聲音和主播的聲音混合等等. 總之將多路聲音疊加, 對豐富移動直播的內(nèi)容起到了很重要的作用, 是現(xiàn)在移動直播APP必備的基礎(chǔ)功能之一了。
最初的時候, 一般是用其他APP將音樂播放出來, 再通過麥克風(fēng)采集, 利用聲音在空氣中自然的疊加完成混合偷线。這種做法有如下缺點(diǎn):
- 主播需要在其他音樂播放器APP中操作磨确,比較繁瑣;
- 音樂的聲音也會因?yàn)椴杉a(chǎn)生損耗声邦;
- 而且主播帶上耳機(jī)的話,自然混音就無效了乏奥;
其實(shí)所謂混音就是將不同來源的聲音混合在一起, 變成一路音頻信號。移動直播顯然無法使用專門的高大上設(shè)備來做亥曹,那只能在APP內(nèi)來完成邓了。下面我們來看看如何實(shí)現(xiàn)移動平臺上的混音功能。
2. 方案選擇
在沒有混音的場景下, 我們之前的音頻通路是媳瞪,音頻采集模塊將采集到的PCM音頻數(shù)據(jù)送入到音頻編碼器編碼為AAC進(jìn)行直播驶悟。當(dāng)加入混音后,我們面對的是多路音頻輸入材失,經(jīng)過混合后得到一路音頻送入音頻編碼器進(jìn)行編碼。
要在iOS上實(shí)現(xiàn)以上功能, 有好幾種方法可以實(shí)現(xiàn):
- 系統(tǒng)API來做混音, iOS系統(tǒng)提供了多個層次的音頻處理接口:AudioUnit硫豆、AudioToolBox龙巨、AVFoundation等,從最底層開始就提供了混音模塊, 理論上都能能夠?qū)崿F(xiàn)聲音混合功能熊响。
- 使用第三方或自己實(shí)現(xiàn)的音頻混合算法旨别。
使用系統(tǒng)API的方案網(wǎng)上的例子比較多, 參考Apple的示例代碼和文檔就好, 這里不詳細(xì)寫。我們主要通過自己實(shí)現(xiàn)的混音模塊來看看混音具體需要做些啥汗茄,并且這種方案對輸入和輸出數(shù)據(jù)的要求最低秸弛,和其他模塊的集成方式最靈活,可以直接以PCM數(shù)據(jù)的形式輸入和輸出。按照相同的實(shí)現(xiàn)思路,也可以跨平臺使用递览。
3. 音頻數(shù)據(jù)
音頻數(shù)據(jù)有兩種存在形式, 一種是壓縮后的, 比如MP3叼屠、AAC等格式的錄音或音樂文件; 另一種是未壓縮的PCM數(shù)據(jù),也就是我們用數(shù)字的形式對自然界中聲音的表示绞铃。聲音的波形非常復(fù)雜,為了描述和記錄聲音,通常我們采用脈沖代碼調(diào)制編碼(PCM編碼). PCM編碼通過抽樣,量化等步驟將連續(xù)的模擬信號轉(zhuǎn)換為離散的數(shù)字信號镜雨。
簡單來講PCM數(shù)據(jù)的格式信息包括如下三個:
- 采樣率;
- 每個sample的數(shù)據(jù)類型儿捧;
- 通道數(shù)和通道排列方式荚坞;
采樣率說的是每秒鐘采樣多少個sample, 也就是一秒鐘的聲音有多少數(shù)據(jù)來描述。采樣率越高表明對聲音的還原度越高菲盾,聲音質(zhì)量就越好颓影,但是相應(yīng)的數(shù)據(jù)量也越大。常見的采樣率是16KHz懒鉴、44.1KHz诡挂、48KHz等。
每個sample的數(shù)據(jù)類型, 比如是否有符號,整數(shù)還是浮點(diǎn)數(shù), 幾個bit表示等. 通常我們在iOS上處理的都是16bit的無符號整數(shù)的音頻疗我。
通道數(shù)主要為了產(chǎn)生立體聲, 現(xiàn)實(shí)中人能夠聽聲辯位主要是靠聲音到達(dá)左右耳的時間差, 在聽音樂時候,如果能夠還原出這種差異,就能有身臨其境的感覺.所以就有了雙通道甚至多通道的音頻數(shù)據(jù). 通道的排列方式主要是說多通道的數(shù)據(jù)是說每個sample交織著放在一起, 還是不同通道的數(shù)據(jù)分塊存放咆畏。
混音算法的處理對象就是這些PCM音頻數(shù)據(jù)。
4. 混音算法
其實(shí)混音的核心部分本來是音頻的疊加, 這一塊的算法和論文都蠻多的, 要實(shí)現(xiàn)好確實(shí)比較難吴裤。但是在直播場景中, 需要混合的聲音路數(shù)比較少, 每一路的音量電平都不太高, 可以直接使用每路音頻的PCM數(shù)據(jù)求和就可以了旧找。
為了調(diào)整每一路聲音在最后聲音中的音量大小,給每個sample乘以一個系數(shù),再做求和.這樣最后采樣的混音算法其實(shí)就是加權(quán)求和。
5. 混音模塊
在選定了混音算法之后, 就需要將該算法封裝為模塊, 集成到直播SDK中. 當(dāng)考慮到實(shí)際的場景的時候, 就面臨了新的問題, 比如聲音格式輸入格式的多樣性. 當(dāng)兩路聲音的格式不同時, 我們就需要對數(shù)據(jù)進(jìn)行轉(zhuǎn)換,得到相同的格式才能最終進(jìn)行PCM的加權(quán)求和. 比如采樣率不同, 那每一秒的數(shù)據(jù)的長度都不一樣, 無法進(jìn)行處理. 當(dāng)兩路聲音產(chǎn)生的頻率不同時,也就是每次送入的sample數(shù)量不同,我們就需要增加buffer來對輸入的數(shù)據(jù)進(jìn)行緩存,比如有的33毫秒一次, 有的50毫秒一次, 那一次就只能處理33毫秒的數(shù)據(jù),50毫秒的數(shù)據(jù)就需要緩存一段, 等下次再處理麦牺。
當(dāng)將以上提到的差異進(jìn)行了處理就得到了如下的模塊結(jié)構(gòu)钮蛛。
每一路音頻, 送入混音模塊時,先按照輸出的的格式要求進(jìn)行格式轉(zhuǎn)化(resample), 然后送入pcm buffer中, 輸出模塊, 對每個buffer的長度進(jìn)行監(jiān)控, 當(dāng)滿足輸出條件時, 從每個buffer中取出相同長度的數(shù)據(jù), 進(jìn)行混合,最后輸出剖膳。
其中resampler 可以直接使用ffmpeg中的swresample
庫來完成, PCM buffer可以使用循環(huán)buffer或者fifo來實(shí)現(xiàn).
6. 遇到的坑
- 輸出的聲音有周期性咔咔聲
在實(shí)現(xiàn)混音模塊中,當(dāng)輸入和輸出的每次處理的sample數(shù)不同時,需要考慮多種組合的情況,當(dāng)時漏掉了, 輸入比輸出慢的情況, 導(dǎo)致出現(xiàn)buffer常滿,數(shù)據(jù)溢出丟棄,最后觀眾端聲音總是會周期性的咔咔聲. 主要要考慮以下三種情況:
- 輸入和輸出數(shù)據(jù)的采樣率相同魏颓,此時送入數(shù)據(jù)和回調(diào)數(shù)據(jù)的頻率基本一致;
- 輸入數(shù)據(jù)采樣率低于輸出數(shù)據(jù)吱晒,此時輸入數(shù)據(jù)頻率比回調(diào)數(shù)據(jù)的頻率低甸饱;
- 輸入數(shù)據(jù)采樣率高于輸出數(shù)據(jù),此時輸入數(shù)據(jù)頻率比回調(diào)數(shù)據(jù)的頻率高仑濒;
要能夠兼容前兩種情形叹话,我們只需在每次輸入數(shù)據(jù)時檢查buffer中是否有足夠輸出的數(shù)據(jù)量,而要兼容第三種情形墩瞳,則需要內(nèi)部按照buffer中剩余數(shù)據(jù)的長度驼壶,定時向外刷回調(diào)數(shù)據(jù)。當(dāng)前的邏輯是兩者同時存在喉酌,每次數(shù)據(jù)輸入時檢查热凹,并且當(dāng)buffer中剩余數(shù)據(jù)足夠一次輸出時也定時回調(diào)泵喘。
2.只要在xcode中打斷點(diǎn),命中后繼續(xù)執(zhí)行就會出現(xiàn)崩潰
我們模塊中的PCMbuffer是采用的 TPCircularBuffer, 原本我們對buffer進(jìn)行寫入操作時, 只是通過 TPCircularBufferHead
接口判斷還有多少空間, 而沒有檢查返回的header指針的有效性。
當(dāng)命中斷點(diǎn)的時候般妙,偶現(xiàn)字節(jié)數(shù)正確纪铺,而header失效的情況,導(dǎo)致在調(diào)試階段出現(xiàn)崩潰股冗,而實(shí)際運(yùn)行過程中沒問題的情況霹陡。該問題只影響了debug,但是具體原因未知止状。
7. 展望
這里采用的混音算法是簡單粗暴的加權(quán)求和, 當(dāng)路數(shù)增加或者其中某一路的音量很高時, 直接求和就會出現(xiàn)超過每個sample所能表示的范圍而溢出,造成爆音烹棉,如果要應(yīng)付更豐富的場景,可能需要更強(qiáng)的算法來支持怯疤。
也歡迎大家使用我們的直播浆洗、短視頻SDK。金山云SDK倉庫地址:
金山云SDK相關(guān)的QQ交流群:
- 視頻云技術(shù)交流群:574179720
- 視頻云iOS技術(shù)交流:621137661