音频延迟

延迟是指信号穿过系统所需的时间。这些是与音频应用相关的常见延迟类型。

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

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

此页面介绍如何开发具有低延迟输入和输出的音频应用,以及如何避免预热延迟。

测量延迟

很难单独测量音频输入和输出延迟,因为它需要准确知道第一个样本何时发送到音频路径(尽管可以使用光测试电路和示波器来做到这一点)。如果您知道往返音频延迟,则可以使用粗略的经验法则:在没有信号处理的路径上,音频输入(和输出)延迟是往返音频延迟的一半

往返音频延迟因设备型号和 Android 版本而异。您可以通过阅读已发布的测量结果,大致了解 Nexus 设备的往返延迟。

您可以通过创建一个生成音频信号、侦听该信号并测量发送和接收之间的时间的应用来测量往返音频延迟。

由于在信号处理最少的音频路径上可以实现最低延迟,因此您可能还想使用音频环回适配器,它允许在耳机连接器上运行测试。

最大程度减少延迟的最佳实践

验证音频性能

Android 兼容性定义文档 (CDD) 列出了兼容 Android 设备的硬件和软件要求。有关整体兼容性计划的更多信息,请参阅Android 兼容性,有关实际 CDD 文档,请参阅CDD

在 CDD 中,往返延迟指定为 20 毫秒或更低(即使音乐家通常需要 10 毫秒)。这是因为 20 毫秒启用了重要的用例。

目前尚无可在 Android 设备上运行时确定任何路径上音频延迟的 API。但是,您可以使用以下硬件功能标志来了解设备是否对延迟有任何保证。

报告这些标志的标准在 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 标称采样率从设备上的麦克风捕获音频,并在 USB 音频上以 48,000 Hz 标称采样率播放音频,则实际采样率很可能彼此略有不同。

潜在独立音频时钟的一个后果是需要异步采样率转换。一种简单的(尽管对于音频质量来说并不理想)异步采样率转换技术是在零交叉点附近根据需要复制或删除样本。也可以进行更复杂的转换。

最大程度地减少输入延迟

本节提供了一些建议,可帮助您在使用内置麦克风或外部耳机麦克风录制时减少音频输入延迟。

  • 如果您的应用正在监控输入,建议您的用户使用耳机(例如,在首次运行时显示“使用耳机效果最佳”屏幕)。请注意,仅使用耳机并不能保证最低可能的延迟。您可能需要执行其他步骤以从音频路径中移除任何不需要的信号处理,例如在录制时使用 VOICE_RECOGNITION 预设。
  • 准备好处理 getProperty(String) 报告的 44,100 和 48,000 Hz 标称采样率,用于 PROPERTY_OUTPUT_SAMPLE_RATE。其他采样率是可能的,但很少见。
  • 准备好处理 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(硬件抽象层)缓冲区可以容纳的音频帧数。您应该构建音频缓冲区,使其包含此数字的精确倍数。如果您使用正确的音频帧数,您的回调将以规律的间隔发生,从而减少抖动。

使用 API 确定缓冲区大小而不是使用硬编码值非常重要,因为 HAL 缓冲区大小在不同设备和 Android 版本之间有所不同。

不要添加涉及信号处理的输出接口

快速混音器仅支持这些接口

  • 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. 现在,从您的应用播放一些音频。您大约有 3 秒的时间从终端运行以下命令
  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. 往返延迟时间