在Unity上面做音游,當在移動端實機運行起來,會發現,音頻的發出會有一定的延遲,無論是長音效還是短音效,Unity內置的Audio內部使用的是FMOD,有以下手段改善
通過設置稍微改善其延遲的問題
Edit → Project Settings → Audio → 設置DSP Buffer size為Best latency(設置 dsp 緩沖區大小以優化延遲或性能,設置一個不合適的值會導致安卓設備的電流音)
音頻文件的Load Type為Decompress On Load(讓音頻提前讀取到緩存中)
讓音頻文件越小越好
代碼來獲取準確的音軌采樣時間
糟糕的方式
AudioSource audioSource;
//100ms延遲
float GetTrackTime()
{
return audioSource.time;
}
好的方式
//20ms延遲
float GetTrackTime()
{
return 1f * audioSource.timeSamples / audioSource.clip.frequency;
}
更好的方式
double trackStartTime;
void StartMusic()
{
trackStartTime = AudioSettings.dspTime + 1;
audioSource.PlayScheduled(trackStartTime);
}
double GetTrackTime()
{
return AudioSettings.dspTime - trackStartTime;
}
最好的方式
Stopwatch stopwatch = new Stopwatch();
void StartMusic()
{
audioSource.Play();
stopwatch.Start();
}
double GetTrackTime()
{
return stopwatch.ElapsedMilliseconds/1000f;
}
//timeSample的Get方法受限與音頻異步的問題是有20ms的延遲的,但是Set方法幾乎沒有延遲
void SetTrackTime(float time)
{
audioSource.timeSample = (int)(time * audioSource.clip.frequency);
}
但是結果往往還是無法讓人滿意,經過測試,iOS大多數設備的延遲降到10ms以內,誤差范圍外,相當于沒有了,但是安卓設備的延遲根據機型的不同有不同的延遲,大約在100ms~500ms:
image.png
硬件和軟件的原因造成了Android設備的延遲偏高以及不統一
音頻播放的不同階段
模擬音頻輸入
可能有幾種不同的模擬組件,例如內置麥克風的前置放大器。在這種情況下,這些模擬組件可被視為“零延遲”,因為它們的真實延遲通常低于1 ms。
延遲:0
模數轉換(ADC)
音頻芯片以預定義的間隔測量輸入的音頻流,并將每個測量值轉換為數字。此預定義間隔稱為采樣率,以Hz為單位。我們的移動音頻普查和延遲測試應用程序顯示,48000 Hz是Android和iOS設備上大多數音頻芯片的原生采樣率,這意味著音頻流每秒采樣48000次。
由于ADC實現通常在內部包含過采樣濾波器,因此經驗法則是將ADC步長歸因于1 ms延遲。
現在音頻流已經數字化,從這一點開始,音頻流現在是數字音頻。數字音頻幾乎不會一個接一個地傳播,而是以塊狀稱,稱為“緩沖區”或“周期”。
延遲:1毫秒
總線從音頻芯片傳輸到音頻驅動器
音頻芯片有幾個任務。它處理ADC和DAC,在多個輸入和輸出之間切換或混合,應用音量等。它還將離散數字音頻樣本“分組”到緩沖區中,并處理這些緩沖區到操作系統的傳輸。
音頻芯片通過總線連接到CPU,例如USB,PCI,Firewire等。每個總線都有自己的傳輸延遲,具體取決于其內部緩沖區大小和緩沖區計數。此處的延遲通常為1 ms(內部系統總線上的音頻芯片)至6 ms(具有保守USB總線設置的USB聲卡)。
延遲:1-6毫秒
音頻驅動程序(ALSA,OSS等)
音頻驅動器使用音頻芯片的本機采樣率(大多數情況下為48000 Hz)以“總線緩沖區大小”步驟將輸入音頻接收到環形緩沖器中。
此環形緩沖區在平滑總線傳輸抖動(“粗糙度”)中起著重要作用,并將總線傳輸緩沖區大小“連接”到操作系統音頻堆棧的緩沖區大小。從環形緩沖區消耗數據發生在操作系統音頻堆棧的緩沖區大小中,因此它自然會增加一些延遲。
Android運行在Linux的“頂部”,大多數Android設備使用最流行的Linux音頻驅動程序系統ALSA(高級Linux聲音架構)。ALSA像這樣處理環形緩沖區:
音頻以“周期大小”步驟從環形緩沖器中消耗。
環形緩沖區的大小是“周期大小”的倍數。
例如:
周期大小= 480個樣本。
期間數= 2。
環形緩沖區的大小為480x2 = 960個樣本。
音頻輸入接收到一個周期(480個樣本),而音頻堆棧讀取/處理另一個周期(480個樣本)。
延遲= 1個周期,480個樣本。它等于48000 Hz時的10 ms。
環形緩沖液(960個樣品)
期間(480個樣本) 期間(480個樣本)
常見的周期數為2,但有些系統可能會更高。
延遲:一個或多個時期
Android音頻硬件抽象層(HAL)
HAL充當Android媒體服務器和Linux音頻驅動程序之間的中間人。在將Android“移植”到設備上時,移動設備的制造商提供HAL實現。
實現是開放的,供應商可以自由創建任何類型的HAL代碼。使用預定義的結構進行與媒體服務器的通信。媒體服務器加載HAL并要求創建具有可選首選參數的輸入或輸出流,例如采樣率,緩沖區大小或音頻效果。
注意:HAL可能會也可能不會根據參數執行,并且媒體服務器必須“適應”HAL。
典型的HAL實現是tinyALSA,用于與ALSA音頻驅動程序通信。一些供應商在這里提供了封閉的源代碼來實現他們認為重要的音頻功
在分析Android源存儲庫中的許多開源HAL實現的代碼之后,我們發現了一些怪癖,由于奇怪的配置和糟糕的編碼而不必要地增加了音頻路徑的大量延遲和CPU負載。
一個好的HAL實現不應該添加任何延遲。
延遲:0個或更多樣本
Audio Flinger
Android媒體服務器包含兩項服務:
AudioPolicy服務處理音頻會話和權限處理,例如啟用麥克風訪問或呼叫中斷。它與iOS的音頻會話處理非常相似。
Audio Flinger服務處理數字音頻流。
Audio Flinger創建了一個RecordThread,它充當應用程序和音頻驅動程序之間的中間人。它的基本工作是:
使用Android HAL從驅動程序的環形緩沖區中獲取下一個輸入音頻緩沖區。
如果應用程序請求的采樣率與本機采樣率不同,則重新采樣緩沖區。
如果應用程序請求的緩沖區大小不同于本機周期大小,則執行其他緩沖。
如果按照這種方式配置Android,音頻Flinger有一個“快速混音器”路徑。如果用戶應用程序使用本機(Android NDK)代碼并設置具有本機硬件采樣率和周期大小的音頻緩沖區隊列,則不會在此步驟中進行重新采樣,額外緩沖或混合(“MixerThread”)。
RecordThread使用“推”方法,沒有與音頻驅動程序的任何嚴格同步。它試圖在醒來和跑步時進行“有根據的猜測”,但“推”方法對輟學者更敏感。低延遲系統始終使用“拉”方法,其中音頻驅動程序通過整個音頻鏈“指示”音頻i / o。很明顯,當最初構思,設計和開發Android OS時,低延遲音頻不是優先考慮的事情。
延遲:1個周期(最佳情況)
Binder
Android主進程間通信系統中的共享內存用于在Audio Flinger和用戶應用程序之間傳輸音頻緩沖區。它是Android的核心,在Android內部隨處使用。
延遲:0
AudioRecord
我們現在處于用戶應用程序的過程中。AudioRecord實現音頻輸入的應用程序端。這是一個可通過OpenSL ES訪問的客戶端庫功能。
AudioRecord運行一個線程,定期從Audio Flinger獲取一個新緩沖區,其音頻Flinger描述了“推送”理念。如果開發人員將其設置為僅使用一個緩沖區,則不會為音頻路徑添加延遲。
延遲:0+樣本
用戶應用程序
最后,音頻輸入到達其目的地,即用戶應用程序。
由于輸入和輸出線程不相同,因此用戶應用程序必須在線程之間實現環形緩沖區。它的大小最小為2個周期(1個用于音頻輸入,1個用于音頻輸出),但寫得不好的應用程序通常使用暴力并使用更多周期來解決CPU瓶頸。
從這一點開始,我們開始帶著一些音頻輸出返回。
延遲:超過1個周期,通常接近2個(最佳情況)
AudioTrack
AudioTrack實現音頻輸出的用戶應用程序。這是一個可通過OpenSL ES訪問的客戶端庫功能。它運行一個線程,定期將下一個音頻緩沖區發送到Audio Flinger。在Android 4.4.4之后,AudioTrack不會為音頻路徑添加延遲,因為它可以設置為僅使用一個緩沖區。
延遲:0+樣本
Audio Flinger
創建一個PlaybackThread,它作為音頻輸入中描述的RecordThread的反轉。
延遲:1期(最佳情況)
Android音頻HAL
與音頻輸入相同。
延遲:0個或更多樣本
音頻驅動程序(ALSA,OSS等)
音頻驅動器中的音頻輸出與音頻輸入的工作方式相同,也使用環形緩沖器。
延遲:一個或多個時期
總線從音頻驅動器傳輸到音頻芯片
與音頻輸入的總線傳輸類似,此處的延遲通常為1 ms至6 ms。
延遲:1-6毫秒
數模轉換(DAC)
在這一點上,ADC的反轉,數字音頻被“轉換”回模擬。出于與ADC相同的原因,經驗法則是假設DAC有1 ms的延遲。
延遲:1毫秒
模擬音頻輸出
DAC的輸出信號是模擬音頻,但它需要額外的組件來驅動連接的設備,如耳機。與模擬音頻輸入類似,模擬組件可被視為“零延遲”。
延遲:0
解決方案
在所有階段中,除非重寫安卓底層音頻系統,否則我們開發者能夠操作的部分只有音頻的播放方式,目前安卓原生的播放方式有三種:
MediaPlayer
SoundPool
AudioTrack(OpenSL)
AAudio
第一種用于長音頻播放,實際測試結果為音頻延遲依然十分大100ms~500ms之間
第二種和第三種用于短音頻播放,短音頻的播放延遲得到了很大的改善,基本徘徊在50ms之間
但是由于無法應用于長音頻的播放,問題依舊還是沒得到解決
image.png
現有的解決方案推薦
superpowered
致力于安卓低延遲做底層開發的C++ API
NativeAudio
Unity插件,泰國音頻大佬
Unity2019
聽說2019優化了底層音頻的播放機制
關于長音頻的延遲在各個機型上的不同而無法自動修正的解決方案
收集各種機型預設一個延遲的值
設計一個體驗良好的界面可以幫助用戶設置這個延遲的值
Unity2017默認音頻、NativeAudio(OpenSL)、Criware(OpenSL)、Unity2019(OpenSL)默認音頻延遲比較
短音效
單位是10ms
短音效延遲上NativeAudio和Criware是最低的,也是差不多的。
長音效
誤差范圍內的差距
效率待實驗。