OpenSL ES 程式設計注意事項

本節的注意事項是 OpenSL ES 1.0.1 規範的補充說明。

物件和介面初始化

新開發人員可能會不太瞭解 OpenSL ES 程式設計模型中物件與介面之間的差別,也不熟悉初始化序列。

簡單來說,OpenSL ES 物件與程式設計語言 (例如 Java 和 C++) 中物件的概念類似,兩者間的差別在於 OpenSL ES 物件只能透過相關聯的介面查看,這包括所有物件的初始介面,稱為 SLObjectItf。物件本身沒有控制代碼,只有物件的 SLObjectItf 介面控制代碼。

系統會先「建立」OpenSL ES 物件,接著傳回 SLObjectItf,然後「執行」,這與常見的程式設計模式類似,即先建構物件 (除了記憶體不足或參數無效原因之外不應失敗),然後再完成初始化 (可能會因為缺少資源而失敗)。執行物件時,系統會視需要為實作項目提供邏輯位置,用來分配其他資源。

在 API 建立物件的過程中,應用程式會指出之後計畫取得的所需介面陣列。請注意,這個陣列不會自動取得介面,而只會表明未來計畫取得這些介面的意圖。介面分成「隱性」或「明確」介面。如果之後就會取得明確介面,則必須在陣列中列出該介面,隱性介面則不需列在物件建立陣列中,但列出隱性介面也沒有壞處。OpenSL ES 還有另一種「動態」介面,這種介面不需要在物件建立陣列中指定,因此可在物件建立後新增。為避免複雜,Android 實作項目提供了便利的功能,詳情請參閱「建立物件時使用動態介面」。

建立並執行物件後,應用程式可在初始 SLObjectItf 上使用 GetInterface,為每項需要的功能取得介面。

最後,物件也可以透過其介面使用,但請注意,部分物件需要進一步設定,尤其應注意,含有 URI 資料來源的音訊播放器需進行更多準備才能偵測連線錯誤。詳情請參閱「預先擷取音訊播放器」一節。

在應用程式使用完物件之後,請明確刪除物件,詳情請參閱下方「刪除」一節。

預先擷取音訊播放器

對於含有 URI 資料來源的音訊播放器,Object::Realize 會分配資源,但不會連線至資料來源 (「準備」) 或開始預先擷取資料。在播放器狀態設為 SL_PLAYSTATE_PAUSEDSL_PLAYSTATE_PLAYING 後,即進行上述動作。

部分資訊可能直到這個序列相對較晚的時段才變為已知,請特別注意,一開始 Player::GetDuration 會傳回 SL_TIME_UNKNOWNMuteSolo::GetChannelCount 則會成功傳回聲道數為零或錯誤結果 SL_RESULT_PRECONDITIONS_VIOLATED。這些 API 會在知道值後傳回適當的值。

其他最初未知的屬性包括取樣率和實際媒體內容類型,其中媒體內容類型是透過檢視內容標頭 (而非應用程式指定的 MIME 類型和容器類型) 確定。這些屬性稍後也會在準備/預先擷取期間確定,但沒有 API 可擷取這些屬性。

預先擷取狀態介面可偵測所有資訊可用的時間,或者應用程式可採週期性輪詢的方式。請注意,部分資訊可能「永遠」無法得知,例如串流播放 MP3 的時間長度。

預先擷取狀態介面也有助於偵測錯誤。註冊回呼並至少啟用 SL_PREFETCHEVENT_FILLLEVELCHANGESL_PREFETCHEVENT_STATUSCHANGE 事件。如果同時傳遞這兩個事件、PrefetchStatus::GetFillLevel 回報數值為零,而且 PrefetchStatus::GetPrefetchStatus 回報 SL_PREFETCHSTATUS_UNDERFLOW,則表示資料來源中有不可復原的錯誤,例如,由於本機檔案名稱不存在或網路 URI 無效,因而無法連線至資料來源。

下一個版本的 OpenSL ES 將針對資料來源的錯誤處理提供更為直接的支援。然而,為了與日後的二進位檔相容,我們會繼續支援現行用來回報不可復原錯誤的方法。

總而言之,以下為建議的程式碼序列:

  1. Engine::CreateAudioPlayer
  2. Object:Realize
  3. Object::GetInterface (針對 SL_IID_PREFETCHSTATUS)
  4. PrefetchStatus::SetCallbackEventsMask
  5. PrefetchStatus::SetFillUpdatePeriod
  6. PrefetchStatus::RegisterCallback
  7. Object::GetInterface (針對 SL_IID_PLAY)
  8. Play::SetPlayStateSL_PLAYSTATE_PAUSEDSL_PLAYSTATE_PLAYING

注意:準備作業和預先擷取作業會在此發生。在這段期間,系統是透過定期狀態更新來呼叫回呼。

刪除

退出應用程式時,請務必刪除所有物件。物件應依照與建立時相反的順序刪除,因為如果物件含有任何相依物件,將此類物件刪除便不安全。舉例來說,請依照下列順序刪除物件:音訊播放器和錄音工具、混音輸出,最後則是引擎。

OpenSL ES 不支援自動垃圾收集或對介面的引用計數。在呼叫 Object::Destroy 後,所有從相關聯物件衍生的現有介面都會變成未定義的介面。

Android OpenSL ES 實作項目不會偵測未正確使用這類介面的情形。如在物件刪除後繼續使用這類介面,可能會導致應用程式當機,或以非預期的方式運作。

在物件刪除序列中,建議您將主要物件介面和所有相關介面明確設為 NULL,避免不小心誤用過時的介面控制代碼。

立體聲平移

使用 Volume::EnableStereoPosition 啟用單聲道源的立體聲平移功能時,總聲功率位準會減少 3 dB,這是為了讓聲道源從一個聲道平移至另一聲道時能保持總聲功率位準恆定。因此,請僅在需要時啟用立體聲定位。詳情請參閱維基百科上有關音訊平移的文章。

回呼和執行緒

一般來說,當實作偵測到事件時,會同步呼叫回呼處理常式。此時間與應用程式非同步,因此請使用非阻塞同步處理機制,控管在應用程式與回呼處理常式之間分享的任何變數的存取權。在範例程式碼中 (例如針對緩衝區佇列的程式碼),我們省略了這項同步處理作業,或者為了簡化作業而使用阻塞同步處理。不過,對於任何實際工作環境程式碼,正確的非阻塞同步處理作業非常重要。

回呼處理常式是從未附加至 Android 執行階段的內部非應用程式執行緒呼叫,因此不能使用 JNI。這些內部執行緒對 OpenSL ES 實作項目的完整性非常重要,因此回呼處理常式也不應封鎖或執行過多工作。

如果您的回呼處理常式需要使用 JNI 或執行與回呼不成比例的工作,處理常式應改為將事件交由其他執行緒處理。可接受的回呼工作負載包括算繪和將下一個輸出緩衝區 (針對音訊播放器) 排入佇列、處理剛剛填入的輸入緩衝區,以及將下一個空緩衝區排入佇列 (針對錄音工具),或者簡單的 API,例如「Get」系列中的大部分 API。如要瞭解工作負載,請參閱下方「效能」一節。

請注意,逆向是安全的:已輸入 JNI 的 Android 應用程式執行緒可直接呼叫 OpenSL ES API,包括封鎖的 API。不過,我們不建議透過主執行緒進行封鎖呼叫,因為這可能導致「應用程式無回應」(ANR)。

如何確定呼叫回呼處理常式的執行緒主要取決於實作。之所以要保留靈活性,是為了在日後進行最佳化,特別是在多核心裝置上的最佳化。

執行回呼處理常式的執行緒在不同的呼叫之間不一定有相同的身分。因此,不要期待 pthread_self() 傳回的 pthread_tgettid() 傳回的 pid_t 會在所有呼叫中保持一致。出於相同原因,請勿使用來自回呼的 pthread_setspecific()pthread_getspecific() 等執行緒局部儲存 (TLS) API。

實作可確保同一物件不會發生同一類別的並行回呼。但在不同執行緒上,相同物件可能有不同類型的並行回呼。

效能

由於 OpenSL ES 是原生的 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 級別 17) 開始,應用程式可以查詢裝置主要輸出串流的平台原生或最佳化輸出取樣率與緩衝區大小。結合前述的功能測試,應用程式現在可自行調整至適當設定,在宣告支援的裝置上達到較低的輸出延遲。

如果是 Android 4.2 (API 級別 17) 以下版本,則必須要有至少兩個以上的緩衝區,才能縮短延遲時間。從 Android 4.3 (API 級別 18) 開始,只要一個緩衝區就能降低延遲。

所有輸出效果的 OpenSL ES 介面都會排除較低延遲的路徑。

建議程序如下:

  1. 確認 API 是否為級別 9 或以上級別,以利確認是否使用了 OpenSL ES。
  2. 使用類似如下的程式碼檢查 android.hardware.audio.low_latency 功能:

    Kotlin

    import android.content.pm.PackageManager
    ...
    val pm: PackageManager = context.packageManager
    val claimsFeature: Boolean = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)
    

    Java

    import android.content.pm.PackageManager;
    ...
    PackageManager pm = getContext().getPackageManager();
    boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);
    
  3. 確認 API 是否為級別 17 或以上級別,以利確認使用了 android.media.AudioManager.getProperty()
  4. 使用類似如下的程式碼,取得這部裝置主要輸出串流的原生/最佳輸出取樣率和緩衝區大小:

    Kotlin

    import android.media.AudioManager
    ...
    val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
    val sampleRate: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
    val framesPerBuffer: String = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
    

    Java

    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 是「字串」。請先檢查是否有空值,然後使用 Integer.parseInt() 轉換為 int。
  5. 現在,使用 OpenSL ES 建立含有 PCM 緩衝區佇列資料定位器的音訊播放器。

注意:您可以使用音訊緩衝區大小測試應用程式,藉此確定音訊裝置上 OpenSL ES 音訊應用程式的原生緩衝區大小和取樣率。您也可以前往 GitHub,查看 audio-buffer-size 範例。

短延遲時間音訊播放器的數量有限。如果您的應用程式需要更多 數個 音訊來源,建議你在應用程式層級混合音訊。在活動暫停後,請務必刪除音訊播放器,因為這些音訊播放器是與其他應用程式共用的全域性資源。

為避免音訊功能出現故障,緩衝區佇列回呼處理常式必須在較短且可預測的時間範圍內執行。這通常表示沒有對互斥鎖、條件或 I/O 作業進行無限制封鎖。請考慮使用「嘗試鎖定」方法、具有逾時設定的鎖定和等待方法,以及非阻塞演算法

無論是算繪下一個緩衝區 (適用於 AudioPlayer),還是使用上一個緩衝區 (適用於 AudioRecord),每個回呼所需的運算時間應大致相同。如果無法確定執行時間長度,或是演算法中含有「爆發式」運算,請避免執行演算法。如果任何特定回呼中所花費的 CPU 作業時間明顯高於平均值,回呼運算作業就會爆發。總而言之,理想情況是讓處理常式的 CPU 執行時間變異數接近零,且處理常式不會無限次造成封鎖。

只有下列輸出方式的音訊延遲可能較低:

  • 裝置上的喇叭。
  • 有線耳罩式耳機。
  • 有線耳機。
  • 線路輸出。
  • USB 數位音訊

在部分裝置上,由於需要處理數位訊號來校正和保護喇叭,因此喇叭延遲會比其他路徑高。

從 Android 5.0 (API 級別 21) 起,特定裝置支援短延遲時間的音訊輸入。如要充分運用這項功能,請依照上文所述先確定輸出支援短延遲時間。支援短延遲時間輸出功能是使用短延遲時間輸入功能的先決條件。接著,使用與輸出相同的取樣率和緩衝區大小建立錄音工具。輸入效果的 OpenSL ES 介面會排除延遲時間較短的路徑。如要降低延遲,必須使用錄音預設值 SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION。這個預設值會停用裝置特定的數位訊號處理程序,避免增加輸入路徑的延遲。如要進一步瞭解錄音預設值,請參閱上方的 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 程式設計語言程式碼僅能做到原生程式碼,反之亦同,兩者唯一的區別在於可用的 API。

使用 OpenSL ES 的應用程式必須要求在執行類似非原生 API 時所需的權限。例如,如果您的應用程式要錄製音訊,就必須擁有 android.permission.RECORD_AUDIO 權限;使用音效的應用程式需要 android.permission.MODIFY_AUDIO_SETTINGS;播放網路 URI 資源的應用程式則需要 android.permission.NETWORK。詳情請參閱有關使用系統權限的文章。

取決於平台版本和實作,媒體內容剖析器和軟體轉碼器可能會在呼叫 OpenSL ES 的 Android 應用程式環境內執行 (硬體轉碼器採抽象化設計,但因裝置而異)。透過格式錯誤內容來利用剖析器和轉碼器的安全漏洞是已知的攻擊向量。建議您只播放可信任來源的媒體,或是將應用程式分區,這樣處理不信任來源媒體的程式碼就能在相對「沙箱化」的環境中執行。例如,您可以在單獨的程序中處理來自不可信來源的媒體。雖然兩種程序仍會在同一個 UID 下執行,但這項分隔機制會讓攻擊較難以得逞。