浅谈Unity中Android、iOS音频延迟

在Unity上面做音游,当在移动端实机运行起来,会发现,音频的发出会有一定的延迟,无论是长音效还是短音效,Unity内置的Audio内部使用的是FMOD,有以下手段改善

通过设置稍微改善其延迟的问题

  • Edit → Project Settings → Audio → 设置DSP Buffer size为Best latency(设置 dsp 缓冲区大小以优化延迟或性能,设置一个不合适的值会导致安卓设备的电流音
  • 音频文件的Load Type为Decompress On Load(让音频提前读取到缓存中)
  • 让音频文件越小越好

代码来获取准确的音轨采样时间

  • 糟糕的方式
AudioSource audioSource;
//100ms延迟
float GetTrackTime()
{
    return audioSource.time;
}
  • 好的方式
//20ms延迟
float GetTrackTime()
{
    return 1f * audioSource.timeSamples / audioSource.clip.frequency;
}
  • 更好的方式
double trackStartTime;
void StartMusic()
{
    trackStartTime = AudioSettings.dspTime + 1;
    audioSource.PlayScheduled(trackStartTime);
}
double GetTrackTime()
{
    return AudioSettings.dspTime - trackStartTime;
}
  • 最好的方式
Stopwatch stopwatch = new Stopwatch();
void StartMusic()
{
    audioSource.Play();
    stopwatch.Start();
}
double GetTrackTime()
{
    return stopwatch.ElapsedMilliseconds/1000f;
}
//timeSample的Get方法受限与音频异步的问题是有20ms的延迟的,但是Set方法几乎没有延迟
void SetTrackTime(float time)
{
    audioSource.timeSample = (int)(time *  audioSource.clip.frequency);
}

但是结果往往还是无法让人满意,经过测试,iOS大多数设备的延迟降到10ms以内,误差范围外,相当于没有了,但是安卓设备的延迟根据机型的不同有不同的延迟,大约在100ms~500ms



image.png


硬件和软件的原因造成了Android设备的延迟偏高以及不统一

音频播放的不同阶段

  • 模拟音频输入
    可能有几种不同的模拟组件,例如内置麦克风的前置放大器。在这种情况下,这些模拟组件可被视为“零延迟”,因为它们的真实延迟通常低于1 ms。
    延迟:0
  • 模数转换(ADC)
    音频芯片以预定义的间隔测量输入的音频流,并将每个测量值转换为数字。此预定义间隔称为采样率,以Hz为单位。我们的移动音频普查和延迟测试应用程序显示,48000 Hz是Android和iOS设备上大多数音频芯片的原生采样率,这意味着音频流每秒采样48000次。
    由于ADC实现通常在内部包含过采样滤波器,因此经验法则是将ADC步长归因于1 ms延迟。
    现在音频流已经数字化,从这一点开始,音频流现在是数字音频。数字音频几乎不会一个接一个地传播,而是以块状称,称为“缓冲区”或“周期”。
    延迟:1毫秒
  • 总线从音频芯片传输到音频驱动器
    音频芯片有几个任务。它处理ADC和DAC,在多个输入和输出之间切换或混合,应用音量等。它还将离散数字音频样本“分组”到缓冲区中,并处理这些缓冲区到操作系统的传输。
    音频芯片通过总线连接到CPU,例如USB,PCI,Firewire等。每个总线都有自己的传输延迟,具体取决于其内部缓冲区大小和缓冲区计数。此处的延迟通常为1 ms(内部系统总线上的音频芯片)至6 ms(具有保守USB总线设置的USB声卡)。
    延迟:1-6毫秒
  • 音频驱动程序(ALSA,OSS等)
    音频驱动器使用音频芯片的本机采样率(大多数情况下为48000 Hz)以“总线缓冲区大小”步骤将输入音频接收到环形缓冲器中。
    此环形缓冲区在平滑总线传输抖动(“粗糙度”)中起着重要作用,并将总线传输缓冲区大小“连接”到操作系统音频堆栈的缓冲区大小。从环形缓冲区消耗数据发生在操作系统音频堆栈的缓冲区大小中,因此它自然会增加一些延迟。
    Android运行在Linux的“顶部”,大多数Android设备使用最流行的Linux音频驱动程序系统ALSA(高级Linux声音架构)。ALSA像这样处理环形缓冲区:
    音频以“周期大小”步骤从环形缓冲器中消耗。
    环形缓冲区的大小是“周期大小”的倍数。
    例如:
    周期大小= 480个样本。
    期间数= 2。
    环形缓冲区的大小为480x2 = 960个样本。
    音频输入接收到一个周期(480个样本),而音频堆栈读取/处理另一个周期(480个样本)。
    延迟= 1个周期,480个样本。它等于48000 Hz时的10 ms。
    环形缓冲液(960个样品)
    期间(480个样本) 期间(480个样本)
    常见的周期数为2,但有些系统可能会更高。
    延迟:一个或多个时期
  • Android音频硬件抽象层(HAL)
    HAL充当Android媒体服务器和Linux音频驱动程序之间的中间人。在将Android“移植”到设备上时,移动设备的制造商提供HAL实现。
    实现是开放的,供应商可以自由创建任何类型的HAL代码。使用预定义的结构进行与媒体服务器的通信。媒体服务器加载HAL并要求创建具有可选首选参数的输入或输出流,例如采样率,缓冲区大小或音频效果。
    注意:HAL可能会也可能不会根据参数执行,并且媒体服务器必须“适应”HAL。
    典型的HAL实现是tinyALSA,用于与ALSA音频驱动程序通信。一些供应商在这里提供了封闭的源代码来实现他们认为重要的音频功
    在分析Android源存储库中的许多开源HAL实现的代码之后,我们发现了一些怪癖,由于奇怪的配置和糟糕的编码而不必要地增加了音频路径的大量延迟和CPU负载。
    一个好的HAL实现不应该添加任何延迟。
    延迟:0个或更多样本
  • Audio Flinger
    Android媒体服务器包含两项服务:
    • AudioPolicy服务处理音频会话和权限处理,例如启用麦克风访问或呼叫中断。它与iOS的音频会话处理非常相似。
    • Audio Flinger服务处理数字音频流。
      Audio Flinger创建了一个RecordThread,它充当应用程序和音频驱动程序之间的中间人。它的基本工作是:
    • 使用Android HAL从驱动程序的环形缓冲区中获取下一个输入音频缓冲区。
    • 如果应用程序请求的采样率与本机采样率不同,则重新采样缓冲区。
    • 如果应用程序请求的缓冲区大小不同于本机周期大小,则执行其他缓冲。
      如果按照这种方式配置Android,音频Flinger有一个“快速混音器”路径。如果用户应用程序使用本机(Android NDK)代码并设置具有本机硬件采样率和周期大小的音频缓冲区队列,则不会在此步骤中进行重新采样,额外缓冲或混合(“MixerThread”)。
      RecordThread使用“推”方法,没有与音频驱动程序的任何严格同步。它试图在醒来和跑步时进行“有根据的猜测”,但“推”方法对辍学者更敏感。低延迟系统始终使用“拉”方法,其中音频驱动程序通过整个音频链“指示”音频i / o。很明显,当最初构思,设计和开发Android OS时,低延迟音频不是优先考虑的事情。
      延迟:1个周期(最佳情况)


  • Binder
    Android主进程间通信系统中的共享内存用于在Audio Flinger和用户应用程序之间传输音频缓冲区。它是Android的核心,在Android内部随处使用。
    延迟:0
  • AudioRecord
    我们现在处于用户应用程序的过程中。AudioRecord实现音频输入的应用程序端。这是一个可通过OpenSL ES访问的客户端库功能。
    AudioRecord运行一个线程,定期从Audio Flinger获取一个新缓冲区,其音频Flinger描述了“推送”理念。如果开发人员将其设置为仅使用一个缓冲区,则不会为音频路径添加延迟。
    延迟:0+样本
  • 用户应用程序
    最后,音频输入到达其目的地,即用户应用程序。
    由于输入和输出线程不相同,因此用户应用程序必须在线程之间实现环形缓冲区。它的大小最小为2个周期(1个用于音频输入,1个用于音频输出),但写得不好的应用程序通常使用暴力并使用更多周期来解决CPU瓶颈。
    从这一点开始,我们开始带着一些音频输出返回。
    延迟:超过1个周期,通常接近2个(最佳情况)
  • AudioTrack
    AudioTrack实现音频输出的用户应用程序。这是一个可通过OpenSL ES访问的客户端库功能。它运行一个线程,定期将下一个音频缓冲区发送到Audio Flinger。在Android 4.4.4之后,AudioTrack不会为音频路径添加延迟,因为它可以设置为仅使用一个缓冲区。
    延迟:0+样本
  • Audio Flinger
    创建一个PlaybackThread,它作为音频输入中描述的RecordThread的反转。
    延迟:1期(最佳情况)
  • Android音频HAL
    与音频输入相同。
    延迟:0个或更多样本
  • 音频驱动程序(ALSA,OSS等)
    音频驱动器中的音频输出与音频输入的工作方式相同,也使用环形缓冲器。
    延迟:一个或多个时期
  • 总线从音频驱动器传输到音频芯片
    与音频输入的总线传输类似,此处的延迟通常为1 ms至6 ms。
    延迟:1-6毫秒
  • 数模转换(DAC)
    在这一点上,ADC的反转,数字音频被“转换”回模拟。出于与ADC相同的原因,经验法则是假设DAC有1 ms的延迟。
    延迟:1毫秒
  • 模拟音频输出
    DAC的输出信号是模拟音频,但它需要额外的组件来驱动连接的设备,如耳机。与模拟音频输入类似,模拟组件可被视为“零延迟”。
    延迟:0


解决方案

在所有阶段中,除非重写安卓底层音频系统,否则我们开发者能够操作的部分只有音频的播放方式,目前安卓原生的播放方式有三种:

  • MediaPlayer
  • SoundPool
  • AudioTrack(OpenSL)
  • AAudio
    第一种用于长音频播放,实际测试结果为音频延迟依然十分大100ms~500ms之间
    第二种和第三种用于短音频播放,短音频的播放延迟得到了很大的改善,基本徘徊在50ms之间
    但是由于无法应用于长音频的播放,问题依旧还是没得到解决


image.png

现有的解决方案推荐

关于长音频的延迟在各个机型上的不同而无法自动修正的解决方案

  • 收集各种机型预设一个延迟的值
  • 设计一个体验良好的界面可以帮助用户设置这个延迟的值

Unity2017默认音频、NativeAudio(OpenSL)、Criware(OpenSL)、Unity2019(OpenSL)默认音频延迟比较

根据以下视频的测试方式,通过音轨的采样图来
链接:https://pan.baidu.com/s/13sxkAWwFqh-9bxRow3DcWA
提取码:lrcl
最后测出如下延迟

短音效




单位是10ms
短音效延迟上NativeAudio和Criware是最低的,也是差不多的。

长音效




误差范围内的差距
效率待实验。

发布于 2019-08-22