11、低延迟音频通路:FastMixer与FastCapture设计、MMAP/NO_IRQ模式、低延迟配置与调优、性能指标

各位好,今天我们聊一个硬核话题——低延迟音频通路。说实话,在Android音频系统里,这是最考验功底的一块。很多开发者觉得AudioFlinger就是个混音器,其实它背后藏着两条完全不同的路径:一条是给普通App用的常规通路,另一条就是咱们今天要讲的低延迟通路。

我当年刚接触这个模块时,也被绕得晕头转向。后来在调优一个K歌App的耳返延迟时,才真正把FastMixer和FastCapture的脉络理清楚。嗯,咱们今天就把它掰开揉碎了讲。

11.1 为什么需要低延迟通路?

先问一个问题:为什么普通通路不够用?

常规的AudioFlinger混音流程,走的是ToneGenerator或MixerThread。数据从App写入共享内存,AudioFlinger定期拉取、混音、再送到底层驱动。这个过程里,缓冲区大小通常是384帧甚至更多,采样率48kHz时,一帧就是约8ms。再加上双缓冲、调度延迟,端到端延迟轻松超过30ms。

30ms是什么概念?你对着麦克风说话,耳机里听到自己的声音慢了半拍,就像在空旷山谷里喊话有回音。对于VoIP、游戏、乐器App来说,这完全不可接受。

所以Google在Android 4.1引入了低延迟通路,核心思路就是:绕过常规混音,走一条更短的路径

关键指标:低延迟通路的端到端延迟目标通常控制在10ms以内,专业音频App甚至要求5ms以下。

11.2 FastMixer与FastCapture设计

FastMixer和FastCapture,是低延迟通路的两个核心组件。它们运行在独立的线程上,优先级极高,甚至绑定了特定的CPU核心。

11.2.1 FastMixer

FastMixer负责输出。它不处理所有App的混音,只处理那些注册了低延迟通路的Track。这些Track通常来自VoIP、游戏或专业音频App。

它的工作流程是这样的:

  1. App通过AudioTrack创建时,指定标志位AUDIO_OUTPUT_FLAG_FAST
  2. AudioFlinger检测到该标志,将Track分配到FastMixer线程。
  3. FastMixer以极小的缓冲区(比如128帧)周期性地拉取数据,直接送到底层驱动。
  4. 如果同时有常规Track存在,FastMixer会从MixerThread获取混音后的数据,再与自己管理的Fast Track数据合并。

这里有个细节:FastMixer的调度周期是固定的,通常等于一次DMA传输的时间。我见过一些设备上,FastMixer的周期被设为2.6ms,缓冲区大小128帧(48kHz下)。

个人经验:我在调优某款游戏手机时发现,FastMixer的线程优先级必须设为ANDROID_PRIORITY_URGENT_AUDIO,否则偶尔会被其他高优先级线程抢占,导致音频断流。这个坑我踩过两次才记住。

11.2.2 FastCapture

FastCapture是输入端的对应组件。它同样运行在高优先级线程上,专门处理低延迟录音需求。

它的设计思路和FastMixer对称:

  • App通过AudioRecord创建时指定AUDIO_INPUT_FLAG_FAST
  • FastCapture直接从驱动读取数据,不经过常规的RecordThread。
  • 数据以极小的缓冲区(如128帧)传递给App。

我记得有一次做耳返功能,App要求录音到播放的延迟低于8ms。如果走常规通路,录音延迟10ms,播放延迟10ms,加起来就20ms了。换成FastCapture+FastMixer,两端各2.6ms,加起来才5.2ms,完全满足需求。

11.3 MMAP/NO_IRQ模式

这是低延迟通路的进阶玩法。MMAP,全称Memory-Mapped Audio,说白了就是让App和驱动共享同一块内存,数据不用拷贝。

常规模式下,数据流是这样的:

App缓冲区 → AudioFlinger拷贝 → 驱动缓冲区 → DMA传输

MMAP模式下:

App缓冲区(MMAP) ↔ DMA直接传输

看到区别了吗?少了一次拷贝,也少了一次上下文切换。延迟能再降1-2ms。

NO_IRQ模式更激进。它让DMA传输完成后不触发中断,而是让App通过轮询或硬件计数器来感知数据就绪。中断处理本身有几十微秒的开销,虽然不大,但在极致低延迟场景下,每一微秒都要抠。

注意:MMAP/NO_IRQ模式需要硬件和驱动同时支持。不是所有SoC都实现了。我在某款低端平板上尝试开启MMAP,结果驱动直接崩溃。嗯,兼容性测试一定要做。

MMAP模式下,App需要调用AAudioStreamBuilder_setBufferCapacityInFrames()来设置缓冲区容量。容量越大,抗抖动能力越强,但延迟也越高。这是个典型的trade-off。

11.4 低延迟配置与调优

配置低延迟通路,主要涉及以下几个参数:

参数 说明 典型值
缓冲区大小 每次DMA传输的帧数 64、128、256帧
采样率 音频采样频率 48000 Hz
周期数 驱动环形缓冲区的周期数 2或3
线程优先级 FastMixer/FastCapture线程的调度优先级 URGENT_AUDIO

调优时,我一般遵循以下步骤:

  1. 先确认硬件能力:查看/proc/asound/card0/pcm0p/sub0/hw_params,确认驱动支持的最小缓冲区大小。
  2. 从128帧开始:128帧在48kHz下约2.6ms,是个比较安全的起点。
  3. 逐步降低缓冲区:降到64帧(1.3ms),测试是否有XRun(缓冲区欠载或溢出)。
  4. 开启MMAP:如果硬件支持,切换到MMAP模式,延迟还能再降。
  5. 调整CPU亲和性:将FastMixer线程绑定到某个专用核心,避免被其他任务干扰。

避坑指南:我曾经把缓冲区降到32帧,延迟确实漂亮,但设备在播放复杂音效时频繁出现爆音。后来发现是DMA传输来不及填充数据。所以调优不能只看延迟,还要考虑系统的整体负载。

11.5 性能指标

衡量低延迟通路的好坏,主要看这几个指标:

  • 端到端延迟:从App写入数据到扬声器发出声音的时间。通常用dumpsys media.audio_flinger查看。
  • XRun次数:缓冲区欠载(Underrun)或溢出(Overrun)的次数。理想情况是0。
  • CPU占用率:低延迟通路虽然优先级高,但不能吃掉太多CPU资源。
  • 抖动(Jitter):每次DMA传输的时间间隔是否稳定。抖动大,音质就会受影响。

我常用的测试工具是AudioFlinger的dumpsystinyplay/tinycap。dumpsys里会输出每个Thread的详细信息,包括缓冲区大小、XRun计数、调度延迟等。

// 查看FastMixer状态
adb shell dumpsys media.audio_flinger | grep -A 20 "FastMixer"

// 输出示例
FastMixer Thread 0x7b4a000000
  Sample rate: 48000 Hz
  Buffer size: 128 frames
  Underrun count: 0
  Overrun count: 0
  Average scheduling delay: 0.3 ms

如果看到Underrun count不为0,说明缓冲区太小或者系统负载太高。这时候要么增大缓冲区,要么优化CPU调度。

11.6 核心逻辑图

下面这张图展示了低延迟通路的整体架构和数据流向。我特意把常规通路也画出来做对比,这样更直观。

低延迟音频通路架构图 App层 常规AudioTrack Fast AudioTrack MMAP AudioTrack AudioRecord AudioFlinger MixerThread FastMixer FastCapture MMAP 共享内存 驱动层 ALSA PCM DMA 缓冲区 DMA 引擎 MMAP 映射 常规通路 Fast通路 MMAP通路

从图上可以清楚看到,FastMixer和FastCapture走的是绿色箭头,绕过了常规的MixerThread。MMAP通路更直接,App和驱动之间通过共享内存交互,连AudioFlinger的拷贝都省了。

好了,低延迟通路的核心内容就这些。说实话,这部分知识光看文档是不够的,一定要动手去调、去测。我建议你找一台设备,打开dumpsys,看看你的FastMixer跑在什么缓冲区大小下,有没有XRun。实践出真知。

公众号:蓝海资料掘金营,微信deep3321