適用於Android的OpenSL ES指南-程式設計注意事項

DamonRen發表於2018-11-01

翻譯自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_pauseSL_PLAYSTATE_PLAYING,就會出現這種情況。

在此序列中,有些資訊可能直到相對較晚的時候才會為人所知。特別是,初始時,Player::GetDuration返回SL_TIME_UNKNOWN還有 MuteSolo::GetChannelCount返回0,或者返回錯誤結果SL_RESULT_PRECONDITIONS_VIOLATED。當為已知時,才返回正確值。

其他最初未知的屬性包括取樣率和基於檢查內容頭的實際的媒體內容型別(與應用程式指定的MIME型別和容器型別相反)。這些也是在準備/預讀取期間稍後確定的,但是沒有api來檢索它們。

預讀取狀態介面對於檢測何時所有可用資訊非常有用,或者您的應用程式可以定期輪詢。注意,一些資訊,例如MP3流的持續時間,可能永遠不會知道。

預取狀態介面對於檢測錯誤也很有用。註冊一個回撥,並至少啟用SL_PREFETCHEVENT_FILLLEVELCHANGESL_PREFETCHEVENT_STATUSCHANGE事件。如果這兩個事件同時交付,PrefetchStatus::GetFillLevel報告0級,PrefetchStatus::GetPrefetchStatus報告SL_PREFETCHSTATUS_UNDERFLOW,那麼這表明資料來源中有一個不可恢復的錯誤。這包括無法連線資料來源,因為本地檔名不存在或網路URI無效。

OpenSL ES的下一個版本預計將新增對資料來源中錯誤處理的更加顯式的支援。然而 ,為了將來的二進位制相容性,我們打算繼續支援當前不可恢復錯誤報告的方法。

總之,推薦的程式碼序列是:

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface for SL_IID_PREFETCHSTATUS
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterface for SL_IID_PLAY
  8. Play::SetPlayState to SL_PLAYSTATE_PAUSED, or SL_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_tgettid()返回的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介面都排除了較低的延遲路徑。

推薦順序如下:

  1. 檢查API級別9或更高,以確認OpenSL ES的使用。
  2. 檢查android.hardware.audio.low_latency特性使用如下程式碼:
import android.content.pm.PackageManager;
...
PackageManager pm = getContext().getPackageManager();
boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
複製程式碼
  1. 檢查API級別17或更高,以確認android.media.AudioManager.getProperty()的使用。
  2. 使用以下程式碼獲得原生或最優輸出取樣率和此裝置的主輸出流的緩衝區大小:
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));//單位緩衝區幀數

複製程式碼

注意,sampleRateframesPerBuffer都是字串。首先檢查null,然後使用Integer.parseInt()將其轉換為int。 5. 現在使用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下執行,但是這種分離確實使攻擊更加困難。


上一篇: 適用於Android的OpenSL ES指南-OpenSL ES的Android擴充套件

相關文章