音频延迟

延迟是信号通过系统所需的时间。以下是与音频应用相关的常见延迟类型

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

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

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

测量延迟

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

音频往返延迟因设备型号和 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 预设。
  • 准备好处理 getProperty(String) 报告的 PROPERTY_OUTPUT_SAMPLE_RATE 标称采样率 44,100 Hz 和 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(硬件抽象层)缓冲区可以容纳的音频帧数。你应该构建你的音频缓冲区,使其包含该数字的精确倍数。如果你使用正确的音频帧数,你的回调将以固定间隔发生,从而减少抖动。

使用 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. 现在,从你的应用播放一些音频。你有大约三秒钟的时间在终端运行以下命令
  5. adb shell dumpsys media.audio_flinger
    
  6. 查找你的进程 ID。如果在 Name 列看到 F,则表示它在低延迟轨道上(F 代表 fast track)。

最小化预热延迟

首次对音频数据进行排队时,设备音频电路需要少量但仍然显著的时间进行预热。为了避免这种预热延迟,你可以对包含静音的音频数据缓冲区进行排队,如下面的代码示例所示

#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. 往返延迟时间