音频延迟

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

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

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

本页面介绍了如何开发输入和输出延迟时间较短的音频应用,以及如何避免出现预热延迟时间。

测量延迟时间

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

往返音频延迟时间根据设备型号和 Android build 的不同而大不相同。您可以阅读发布的测量值,大致了解 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 build 中,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. 往返延迟时间