音频延迟

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

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

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

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

测量延迟

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

往返音频延迟因设备型号和 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 标称采样率从设备上的麦克风捕获音频,并在 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. 往返延迟时间