翻譯自OpenSL ES Programming Notes
本節中的注釋補充了OpenSL ES 1.0.1規范。
對象和接口初始化
OpenSL ES編程模型的兩個方面可能是新開發人員不熟悉的,即對象和接口之間的區別以及初始化順序。
簡單地說,OpenSL ES對象類似于Java和c++等編程語言中的對象概念,只是OpenSL ES對象僅通過其關聯接口可見。這包括所有對象的初始接口,稱為SLObjectItf
。沒有對象本身的句柄,只有對象的SLObjectItf
接口的句柄。
首先創建一個OpenSL ES對象,它返回一個SLObjectItf
,然后實例化它。這類似于常見的編程模式,首先構造一個對象(除非缺少內存或無效參數,否則不會失敗),然后完成初始化(可能由于缺乏資源而失敗)。實例化這步為實例提供了在需要時分配額外資源的邏輯內存。
作為創建對象的API的一部分,應用程序指定了它計劃稍后獲取的所需接口數組。注意,這個數組不會自動獲得接口;它僅僅表明了將來獲取它們的意圖。接口被區分為隱式或顯式。如果以后要獲得顯式接口,則必須在數組中列出它。隱式接口不需要在對象創建數組中列出,但是在那里列出它并沒有害處。OpenSL ES還有一種稱為dynamic的接口,它不需要在對象創建數組中指定,可以在對象創建后添加。Android實現提供了一個方便的特性來避免這種復雜性,這種復雜性在OpenSL ES的Android擴展這篇文章中的對象創建時的動態接口中進行了描述。
在創建和實現對象之后,應用程序應該在SLObjectItf
初始化后使用GetInterface
為它需要的每個特性獲取接口。
最后,該對象可以通過其接口使用,不過請注意,有些對象需要進一步設置。特別是,帶有URI數據源的音頻播放器需要做更多的準備,以檢測連接錯誤。有關詳細信息,請參閱下面的音頻播放器預讀取部分。
應用程序處理完對象后,應該顯式地銷毀它;參見下面的銷毀部分。
音頻播放器預讀取
對于具有URI數據源的音頻播放器,Object::Realize
分配資源,但不連接數據源(準備階段)或開始預讀取數據。一旦將播放器狀態設置為sl_playstate_pause
或SL_PLAYSTATE_PLAYING
,就會出現這種情況。
在此序列中,有些信息可能直到相對較晚的時候才會為人所知。特別是,初始時,Player::GetDuration
返回SL_TIME_UNKNOWN
還有 MuteSolo::GetChannelCount
返回0,或者返回錯誤結果SL_RESULT_PRECONDITIONS_VIOLATED
。當為已知時,才返回正確值。
其他最初未知的屬性包括采樣率和基于檢查內容頭的實際的媒體內容類型(與應用程序指定的MIME類型和容器類型相反)。這些也是在準備/預讀取期間稍后確定的,但是沒有api來檢索它們。
預讀取狀態接口對于檢測何時所有可用信息非常有用,或者您的應用程序可以定期輪詢。注意,一些信息,例如MP3流的持續時間,可能永遠不會知道。
預取狀態接口對于檢測錯誤也很有用。注冊一個回調,并至少啟用SL_PREFETCHEVENT_FILLLEVELCHANGE
和SL_PREFETCHEVENT_STATUSCHANGE
事件。如果這兩個事件同時交付,PrefetchStatus::GetFillLevel
報告0級,PrefetchStatus::GetPrefetchStatus
報告SL_PREFETCHSTATUS_UNDERFLOW
,那么這表明數據源中有一個不可恢復的錯誤。這包括無法連接數據源,因為本地文件名不存在或網絡URI無效。
OpenSL ES的下一個版本預計將添加對數據源中錯誤處理的更加顯式的支持。然而 ,為了將來的二進制兼容性,我們打算繼續支持當前不可恢復錯誤報告的方法。
總之,推薦的代碼序列是:
Engine::CreateAudioPlayer
Object:Realize
-
Object::GetInterface
forSL_IID_PREFETCHSTATUS
PrefetchStatus::SetCallbackEventsMask
PrefetchStatus::SetFillUpdatePeriod
PrefetchStatus::RegisterCallback
-
Object::GetInterface
forSL_IID_PLAY
-
Play::SetPlayState
toSL_PLAYSTATE_PAUSED
, orSL_PLAYSTATE_PLAYING
注意:這里有準備和預讀取;在這段時間內,你的回調會被定期的狀態更新調用。
銷毀
在退出應用程序時,請確保銷毀所有對象。對象應該按照創建對象的相反順序被銷毀,因為銷毀具有任何依賴對象的對象是不安全的。例如,按以下順序銷毀:音頻播放器和錄音機,輸出混合,最后是引擎。
OpenSL ES不支持自動垃圾收集或接口的引用計數。在您調用Object::Destroy
之后,所有從關聯對象派生的現有接口都將無法定義。
Android OpenSL ES實例不會檢測到這些接口的不正確使用情況。在對象被銷毀后繼續使用這些接口可能導致應用程序崩潰或以不可預知的方式運行。
我們建議您顯式地將主對象接口和所有關聯接口都設置為NULL,作為對象銷毀序列的一部分,這樣可以防止對陳舊接口句柄的意外濫用。
立體聲平移
當Volume::EnableStereoPosition
用于啟用單聲道源的立體平移時,總聲波功率級別降低了3分貝。允許總聲波功率水平保持不變是必要的,因為聲源是從一個通道到另一個通道。因此,只有在你需要的時候,才能啟用立體聲定位。有關更多信息,請參閱Wikipedia關于音頻平移的文章。
回調和線程
當實例檢測到事件時,通常同步調用回調處理程序。對于應用程序,這一點是異步的,因此應該使用非阻塞同步機制來控制應用程序和回調處理程序之間共享變量的訪問權限。在示例代碼(例如緩沖區隊列)中,為了簡單起見,我們要么省略了這個同步,要么使用了阻塞同步。然而,適當的非阻塞同步對于任何代碼都是至關重要的。
回調處理程序是從內部非應用程序線程調用的,這些線程不attach到Android runtime ,因此它們不具備使用JNI的資格。因為這些內部線程對OpenSL ES實例的完整性至關重要,所以回調處理程序也不應該阻塞或執行過多的工作。
如果回調處理程序需要使用JNI或執行與回調不相稱的工作,則處理程序應該向另一個線程發布一個事件來處理。可接受的回調工作負載的示例包括渲染和排隊下一個輸出緩沖區(用于AudioPlayer)、處理剛剛填充的輸入緩沖區和排隊下一個空緩沖區(用于AudioRecorder)或簡單api(如Get系列的大部分)。關于工作負載,請參閱下面的性能部分。
注意,反過來是安全的:已使用JNI的Android應用程序線程可以直接調用OpenSL ES api,包括那些阻塞的api。但是,主線程不建議使用阻塞調用,因為它們可能導致應用程序不響應(ANR)。
關于調用回調處理程序的線程的決定很大程度上取決于OpenSL ES實現。這種靈活性的原因是為了允許將來進行優化,特別是在多核設備上。
回調處理程序運行的線程不能保證在不同調用之間具有相同的標識。因此,不要依賴pthread_self()
返回的pthread_t
或gettid()
返回的pid_t
在調用之間保持一致。出于同樣的原因,不要從回調中使用線程本地存儲(TLS) api,例如pthread_setspecific()
和pthread_getspecific()
。
該實現保證不會對同一對象發生相同類型的并發回調。然而,在不同的線程上,對于相同對象的不同類型的并發回調是可能的。
性能
由于OpenSL ES是一個 native C API,調用OpenSL ES的非運行時應用程序線程沒有與運行時相關的開銷,比如垃圾收集暫停。除了下面描述的一個例外,使用OpenSL ES沒有其他性能優勢。特別是,使用OpenSL ES并不能保證比平臺通常提供的更低的音頻延遲和更高的調度優先級。另一方面,隨著Android平臺和特定設備實現的不斷發展,OpenSL ES應用程序有望從未來的系統性能改進中獲益。
其中一個改進是支持減少音頻輸出延遲。減少輸出延遲的基礎首先包含在Android 4.1 (API級別16)中,然后在Android 4.2 (API級別17)中繼續進行。這些改進可以通過OpenSL ES用于設備實現,這些設備實現聲稱具有android.hardware.audio.low_latency
特性。如果設備沒有聲明這個特性,但是支持Android 2.3 (API級別9)或更高,那么您仍然可以使用OpenSL ES API,但是輸出延遲可能會更高。只有當應用程序請求與設備本機輸出配置兼容的緩沖區大小和采樣率時,才使用較低的輸出延遲路徑。這些參數是特定于設備的,應如下所述獲得。
從Android 4.2 (API level 17)開始,應用程序可以查詢平臺原生或最佳輸出采樣率和設備主輸出流的緩沖區大小。當與剛才提到的特性測試結合使用時,應用程序現在可以適當地配置自己,以在聲稱支持的設備上降低輸出延遲。
對于Android 4.2 (API級別17)及更早版本,為了降低延遲,需要兩個或更多的緩沖區計數。從Android 4.3 (API級別18)開始,一個緩沖區計數就足以降低延遲。
所有用于輸出效果的OpenSL ES接口都排除了較低的延遲路徑。
推薦順序如下:
- 檢查API級別9或更高,以確認OpenSL ES的使用。
- 檢查
android.hardware.audio.low_latency
特性使用如下代碼:
import android.content.pm.PackageManager;
...
PackageManager pm = getContext().getPackageManager();
boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
- 檢查API級別17或更高,以確認
android.media.AudioManager.getProperty()
的使用。 - 使用以下代碼獲得原生或最優輸出采樣率和此設備的主輸出流的緩沖區大小:
import android.media.AudioManager;
...
AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE));//采樣率
String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER));//單位緩沖區幀數
注意,sampleRate
和framesPerBuffer
都是字符串。首先檢查null,然后使用Integer.parseInt()
將其轉換為int。
- 現在使用OpenSL ES創建一個帶有PCM緩沖隊列數據定位器的AudioPlayer。
注意:您可以使用音頻緩沖區大小測試應用程序來確定音頻設備上OpenSL ES音頻應用程序的本機緩沖區大小和采樣率。您還可以訪問GitHub,查看音頻緩沖大小的示例。
低延遲音頻播放器的數量是有限的。如果您的應用程序需要多個音頻源,請考慮在應用程序級別混合音頻。當您的活動暫停時,請確保銷毀您的音頻播放器,因為它們是與其他應用程序共享的全局資源。
為了避免出現可聽見的故障,緩沖區隊列回調處理程序必須在一個小而可預測的時間窗口內執行。這通常意味著對互斥對象、條件或I/O操作沒有不可控制的阻塞。相反,應該考慮使用鎖、鎖和超時等待以及非阻塞算法。
渲染下一個緩沖區(用于AudioPlayer)或使用前一個緩沖區(用于AudioRecord)所需的計算時間應該與每次回調的時間大致相同。避免在不確定的時間內執行的算法,或者在計算中出現問題。如果在任何給定回調中所花費的CPU時間明顯大于平均值,則回調計算就會很激烈。總之,理想的情況是處理程序的CPU執行時間接近于零,處理程序在不設限時間內不阻塞。
只對以下輸出做到低延遲音頻是可能的:
- 設備內置揚聲器。
- 有線耳麥。
- 有線耳機。
- 線路輸出(音響)。
- USB數字音頻。
在某些設備上,由于需要對揚聲器進行校正和保護的數字信號處理,揚聲器等待時間比其他路徑要長。
在某些設備上,由于需要對揚聲器進行校正和維護及數字信號處理,揚聲器等待時間比其他路徑要長。
從Android 5.0 (API Level 21)開始,被選的設備支持較低的音頻輸入延遲。要利用這個特性,首先要確認可以使用上面描述的較低的延遲輸出。低延遲輸出的能力是低延遲輸入特性的先決條件。然后,創建一個AudioRecorder,其采樣率和緩沖區大小與用于輸出的相同。用于輸入效果的OpenSL ES接口排除了較低的延遲路徑。為了降低延遲,record預設必須使用 SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION
;此預設將禁用特定于設備的數字信號處理,這可能會增加輸入路徑的延遲。有關record預置的更多信息,請參閱OpenSL ES的Android擴展這篇文章中的Android配置接口部分。
對于同時輸入和輸出,每一方都使用單獨的緩沖區隊列完成處理程序。沒有保證這些回調的相對順序,或音頻時鐘的同步,即使雙方使用相同的采樣率。應用程序應該使用適當的緩沖區同步來緩沖數據。
可能獨立的音頻時鐘的一個后果是需要異步采樣率轉換。異步采樣率轉換的一種簡單(雖然不太理想)技術是在零交叉點附近復制或刪除采樣。更復雜的轉換也是可能的。
性能模式
從Android 7.1 (API級別25)開始,OpenSL ES引入了一種方法來指定音頻路徑的性能模式。
選項是:
-
SL_ANDROID_PERFORMANCE_NONE
:沒有特定的性能要求。允許硬件和軟件效果。 -
SL_ANDROID_PERFORMANCE_LATENCY
:優先考慮延遲。沒有硬件或軟件的效果。這是默認模式。 -
SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS
:優先考慮延遲,同時仍然允許硬件和軟件效果。 -
SL_ANDROID_PERFORMANCE_POWER_SAVING
:優先考慮節約能源。允許硬件和軟件效果。
注意:如果您不需要低延遲路徑,并且希望利用設備內置的音頻效果(例如提高視頻播放的音質),那么您必須顯式地將性能模式設置為
SL_ANDROID_PERFORMANCE_NONE
。
要設置性能模式,必須使用Android配置接口調用SetConfiguration
,如下所示:
// Obtain the Android configuration interface using a previously configured SLObjectItf.
SLAndroidConfigurationItf configItf = nullptr;
(*objItf)->GetInterface(objItf, SL_IID_ANDROIDCONFIGURATION, &configItf);
// Set the performance mode.
SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_NONE;
result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,
&performanceMode, sizeof(performanceMode));
安全與權限
至于誰能做什么,Android的安全是在進程級別完成的。Java編程語言代碼沒有比原生代碼做更多,原生代碼也沒有能比Java編程語言代碼做更多事。它們之間唯一的區別是可用的api。
使用OpenSL ES的應用程序必須請求對類似的非原生api所需的權限。例如,如果您的應用程序錄制音頻,那么它需要android.permission。RECORD_AUDIO
權限。使用音頻效果的應用程序需要android.permission.MODIFY_AUDIO_SETTINGS
。運行網絡URI資源的應用程序需要android.permission.NETWORK
。有關更多信息,請參見使用系統權限。
根據平臺的版本和實現,媒體內容解析器和軟件編解碼器可能在調用OpenSL ES的Android應用程序上下文中運行(硬件編解碼器是抽象的,但與設備相關)。為了利用解析器和編解碼器漏洞而設計的畸形內容是一個都知道的攻擊方向。我們建議您只從可靠的來源播放媒體,或者將應用程序分區,以便處理來自不可靠來源的媒體的代碼在一個相對沙箱環境中運行。例如,您可以在一個單獨的進程中處理來自不可靠來源的媒體。雖然這兩個進程仍然在同一個UID下運行,但是這種分離確實使攻擊更加困難。