音訊延遲

延遲是指訊號在系統中傳輸所需的時間。以下是與音訊應用程式相關的常見延遲類型:

  • 音訊輸出延遲是指從應用程式產生音訊樣本,到透過耳機插孔或內建喇叭播放音訊樣本之間經過的時間。
  • 音訊輸入延遲是指從裝置的音訊輸入 (例如麥克風) 接收音訊訊號,到這些音訊資料可供應用程式使用之間經過的時間。
  • 往返音訊延遲是指輸入延遲、應用程式處理時間和輸出延遲的時間總和。

  • 觸控延遲是指從使用者輕觸螢幕到應用程式收到觸控事件之間經過的時間。
  • 預熱延遲是指在首次將資料排入緩衝區時,啟動音訊管道所需的時間。

本頁面說明如何開發輸入和輸出延遲時間較短的音訊應用程式,以及如何避免預熱延遲。

測量延遲時間

要單獨測量音訊輸入和輸出延遲時間並不容易,因為這表示需要確切知道第一個樣本何時傳入音訊路徑 (儘管可以透過光測試電路和示波器做到)。如果知道往返音訊延遲時間,可以使用經驗法則:音訊輸入 (和輸出) 延遲時間是經過無訊號處理路徑的往返音訊延遲時間的一半

往返音訊延遲時間會因裝置型號和 Android 版本而有大幅差異。您可以參閱已發布的測量資料,大致瞭解 Nexus 裝置的往返延遲時間。

如要測量往返音訊延遲時間,您可以建立一個應用程式,產生音訊訊號並監聽該訊號,然後測量傳送與接收訊號之間經過的時間。

由於最少訊號處理的音訊路徑會有最短的延遲時間,因此建議您使用 Audio Loopback Dongle (音訊回送轉接器),讓測試可在耳機連接器上執行。

最大程度縮短延遲時間的最佳做法

驗證音訊效能

Android 相容性定義說明文件 (CDD) 中列出相容 Android 裝置的硬體和軟體需求。 如要進一步瞭解整體相容性計畫,請參閱 Android 相容性;如需實際的 CDD 說明文件,請參閱 CDD

CDD 中將往返延遲時間指定為 20 毫秒或更短的時間 (而音樂家一般要求 10 毫秒),這是因為有些重要用途必須在延遲時間為 20 毫秒的狀況下才能達成。

目前尚無任何 API 可確定 Android 裝置上的路徑在執行階段時的音訊延遲時間。不過,您可以使用下列硬體功能旗標,確認裝置是否針對延遲時間提供保證:

如需瞭解報告這些旗標的條件,請參閱 CDD 中的 5.6 音訊延遲5.10 專業音訊章節。

下文說明如何在 Java 中檢查這些功能:

Kotlin

val hasLowLatencyFeature: Boolean =
        packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)

val hasProFeature: Boolean =
        packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO)

Java

boolean hasLowLatencyFeature =
    getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY);

boolean hasProFeature =
    getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUDIO_PRO);

對於各項音訊功能的關係,android.hardware.audio.low_latency 功能是 android.hardware.audio.pro 的先決條件。裝置可以實作 android.hardware.audio.low_latency (而不是 android.hardware.audio.pro),但相反情況就不適用。

不要針對音訊效能做出任何假設

請避免做出以下假設,以免出現延遲問題:

  • 請勿假設行動裝置的喇叭和麥克風通常都會有良好的傳聲效果。這是因為這類裝置的尺寸較小,傳聲效果通常不佳,因此會加上訊號處理功能來改善音質,而訊號處理功能會造成延遲。
  • 請勿假設輸入和輸出回呼會保持同步。針對同步輸入和輸出,系統會分別使用不同的緩衝區佇列完成處理常式,即使兩邊的取樣率相同,我們亦無法保證這些回呼的相對順序,或音訊時鐘的同步。因此,您的應用程式應透過適當進行緩衝區同步來將資料緩衝。
  • 請勿假設實際取樣率會與額定取樣率完全相同。例如,如果額定取樣率為 48,000 Hz,則音訊時鐘與作業系統 CLOCK_MONOTONIC 的頻率稍微不同也相當正常,這是因為音訊和系統時鐘可能由不同的晶體製成。
  • 請勿假設實際的播放取樣率會與實際擷取取樣率完全相同,特別是在端點位於不同路徑時。例如,如果您以 48,000 Hz 的額定取樣率從裝置上的麥克風擷取資料,並以 48,000 Hz 的額定取樣率在 USB 音訊設備上播放,兩者實際的取樣率可能會略有不同。

音訊時鐘可能彼此獨立,這導致的其中一個後果是必須進行非同步取樣率轉換。如要簡單地進行非同步取樣率轉換 (但音質並不理想),可視需要在零交越點附近重複或減少樣本。此外,也可進行更複雜的轉換。

最大程度縮短輸入延遲時間

本節提供建議,協助您在透過內建麥克風或外接耳機麥克風錄音時縮短音訊輸入延遲時間。

  • 如果您的應用程式會監測輸入,請建議使用者使用耳機 (例如,於初次執行時在螢幕上顯示「使用耳罩式耳機的效果最佳」)。請注意,僅使用耳機不代表延遲時間一定能縮減至最低值。您可能需要執行其他步驟,從音訊路徑移除任何不必要的訊號處理作業,例如在錄音時使用 VOICE_RECOGNITION 預設值。
  • 準備好處理 getProperty(String) 針對 PROPERTY_OUTPUT_SAMPLE_RATE 所回報的 44,100 和 48,000 Hz 的額定取樣率。此外,也可能會有其他取樣率,但很少見。
  • 請準備好處理 getProperty(String) 針對 PROPERTY_OUTPUT_FRAMES_PER_BUFFER 所回報的緩衝區大小。一般來說,緩衝區大小包括 96、128、160、192、240、256 或 512 影格,但也可能有其他值。

最大程度縮短輸出延遲時間

在建立音訊播放器時,請使用最佳取樣率

為了最大程度縮短延遲時間,您必須提供與裝置最佳取樣率和緩衝區大小相符的音訊資料。詳情請參閱減少延遲時間的設計

在 Java 中,您可以從 AudioManager 取得最佳取樣率,如以下程式碼範例所示:

Kotlin

val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val sampleRateStr: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
var sampleRate: Int = sampleRateStr?.let { str ->
    Integer.parseInt(str).takeUnless { it == 0 }
} ?: 44100 // Use a default value if property not found

Java

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String sampleRateStr = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
int sampleRate = Integer.parseInt(sampleRateStr);
if (sampleRate == 0) sampleRate = 44100; // Use a default value if property not found

知道最佳取樣率後,就能在建立播放器時提供具體數值。 這個範例使用 OpenSL ES

// create buffer queue audio player
void Java_com_example_audio_generatetone_MainActivity_createBufferQueueAudioPlayer
        (JNIEnv* env, jclass clazz, jint sampleRate, jint framesPerBuffer)
{
   ...
   // specify the audio source format
   SLDataFormat_PCM format_pcm;
   format_pcm.numChannels = 2;
   format_pcm.samplesPerSec = (SLuint32) sampleRate * 1000;
   ...
}

注意samplesPerSec 是指「每個聲道的取樣率」(1 Hz = 1000 mHz)。

使用最佳緩衝區大小將音訊資料排入佇列

您可以透過 AudioManager API 取得最佳緩衝區大小,做法與取得最佳取樣率類似:

Kotlin

val am = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val framesPerBuffer: String? = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)
var framesPerBufferInt: Int = framesPerBuffer?.let { str ->
    Integer.parseInt(str).takeUnless { it == 0 }
} ?: 256 // Use default

Java

AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
int framesPerBufferInt = Integer.parseInt(framesPerBuffer);
if (framesPerBufferInt == 0) framesPerBufferInt = 256; // Use default

PROPERTY_OUTPUT_FRAMES_PER_BUFFER 屬性表示 HAL (硬體抽象層) 緩衝區可容納的音訊影格數量。您所建構的音訊緩衝區應能容納這個數量的確切倍數。如果使用正確的音訊影格數,則回呼會定期出現,並導致時基誤差減少。

HAL 緩衝區的大小會因裝置和 Android 版本而有所不同,因此請務必使用 API 來判斷緩衝區大小,而非硬式編碼值。

請勿新增涉及訊號處理的輸出介面

fast mixer (快速混音器) 僅支援以下介面:

  • SL_IID_ANDROIDSIMPLEBUFFERQUEUE
  • SL_IID_VOLUME
  • SL_IID_MUTESOLO

以下介面涉及訊號處理,而且會導致快速音軌要求遭拒,因此不得使用:

  • SL_IID_BASSBOOST
  • SL_IID_EFFECTSEND
  • SL_IID_ENVIRONMENTALREVERB
  • SL_IID_EQUALIZER
  • SL_IID_PLAYBACKRATE
  • SL_IID_PRESETREVERB
  • SL_IID_VIRTUALIZER
  • SL_IID_ANDROIDEFFECT
  • SL_IID_ANDROIDEFFECTSEND

在建立播放器時,請務必只新增「快速」介面,如以下範例所示:

const SLInterfaceID interface_ids[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_VOLUME };

確認使用的是短延遲時間音軌

請完成下列步驟,確認是否已成功取得短延遲時間音軌:

  1. 啟動應用程式,然後執行下列指令:
  2. adb shell ps | grep your_app_name
    
  3. 記下應用程式的程序 ID。
  4. 現在請從應用程式播放一些音訊。您有大約三秒的時間可以從終端機執行下列指令:
  5. adb shell dumpsys media.audio_flinger
    
  6. 掃描您的程序 ID。如果「名稱」欄中有「F」,表示此為短延遲時間音軌 (F 代表「快速音軌」)。

最大程度縮短預熱延遲時間

首次將音訊資料排入佇列時,裝置音訊電路的預熱時間不長,但仍需花費大量的時間。為避免這種預熱延遲時間,您可以將包含無聲音訊的音訊資料緩衝區排入佇列,如以下程式碼範例所示:

#define CHANNELS 1
static short* silenceBuffer;
int numSamples = frames * CHANNELS;
silenceBuffer = malloc(sizeof(*silenceBuffer) * numSamples);
    for (i = 0; i<numSamples; i++) {
        silenceBuffer[i] = 0;
    }

在需要產生音訊時,您可以轉為將包含真實音訊資料的緩衝區排入佇列。

注意:持續輸出音訊會大幅增加耗電量。請確定已透過 onPause() 方法停止輸出。 此外,請考慮在使用者閒置一段時間後暫停無聲輸出。

其他程式碼範例

如要下載呈現音訊延遲的範例應用程式,請參閱 NDK 範例

取得更多資訊

  1. 音訊延遲 (適用於應用程式開發人員)
  2. 影響音訊延遲時間的因素
  3. 測量音訊延遲時間
  4. 音訊預熱
  5. 延遲時間 (音訊)
  6. 往返延遲時間