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_UNKNOWN,而 MuteSolo::GetChannelCount 則成功傳回聲道數 0 或者錯誤結果 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) 開始,只需要 1 個緩衝區就能縮短延遲時間。

所有輸出效果的 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 作業進行無限制封鎖。請考慮使用「嘗試鎖定」、設定了逾時的鎖定和等待,以及 非阻塞演算法

針對每個回呼作業,算繪下一個緩衝區 (針對音訊播放器) 或使用先前的緩衝區 (針對錄音工具) 所需的運算時間應大致相同。請避免在無法確定執行時間的情況下執行演算法,或是當中含有「爆發式」運算。如果任何特定回呼中所花費的 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 下執行,但運用分隔機制會讓攻擊更加難以得逞。