音频延迟时间

延迟时间是指信号在系统中传输所需的时间。下面是常见类型的音频应用相关延迟时间:

  • 音频输出延迟时间是指音频样本由应用生成到通过耳机插孔或内置扬声器播放之间经历的时间。
  • 音频输入延迟时间是指音频信号由设备音频输入(例如,麦克风)接收到这些音频数据可供应用使用所经历的时间。
  • 往返延迟时间是指输入延迟时间、应用处理时间和输出延迟时间的总和。

  • 触摸延迟时间是指用户触摸屏幕与触摸事件被应用接收之间的时间。
  • 预热延迟时间是指启动音频管道、数据第一次在缓冲区加入队列所需的时间。

本页面介绍如何在开发音频应用时保证低输入和输出延迟时间,以及如何避免出现预热延迟时间。

测量延迟时间

很难单独测量音频输入和输出延迟时间,因为这需要准确了解第一个样本何时传入音频路径(尽管可以使用光检测电路和示波器完成)。如果您了解往返音频延迟时间,则可使用一般经验法则:音频输入(和输出)延迟时间是经过无信号处理路径的往返音频延迟时间的一半

往返音频延迟时间根据设备型号和 Android 版本号的不同而大不相同。您可以通过阅读发布的测量值大略了解 Nexus 设备的往返延迟时间。

要测量往返音频延迟时间,您可以创建一个应用,让其生成音频信号,侦听该信号,并测量发送与接收信号之间经过的时间。或者,您可以安装此延迟时间测试应用。此应用使用拉森测试执行往返延迟时间测试。您也可以查看延迟时间测试应用的源代码

由于最低延迟时间是在信号处理最少的音频路径上获得,您可能还想使用回环音频适配器,让测试能够通过耳机连接器运行。

最大限度减少延迟时间的最佳做法

验证音频性能

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 预设值)。
  • 准备好处理由针对 PROPERTY_OUTPUT_SAMPLE_RATEgetProperty(String) 报告的名义采样率 44,100 和 48,000 Hz。采样率也有可能是其他值,但这种情况很少见。
  • 准备好处理由针对 PROPERTY_OUTPUT_FRAMES_PER_BUFFERgetProperty(String) 报告的缓冲区大小。典型的缓冲区大小包括 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(硬件抽象层)缓冲区可以容纳的音频帧数量。您构建的音频缓冲区应该能够容纳这个数量的确切倍数。如果使用正确数量的音频帧,会定期出现回调,而这将减少抖动。

使用 API 而不是硬编码值来确定缓冲区大小至关重要,因为在不同的设备及不同的 Android 版本中,HAL 缓冲区大小有所不同。

避免添加涉及信号处理的输出接口

快速混合器仅支持下列这些接口:

  • 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。如果您在 Name 列看到 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. 往返延迟时间