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。
它的工作流程是这样的:
- App通过AudioTrack创建时,指定标志位
AUDIO_OUTPUT_FLAG_FAST。 - AudioFlinger检测到该标志,将Track分配到FastMixer线程。
- FastMixer以极小的缓冲区(比如128帧)周期性地拉取数据,直接送到底层驱动。
- 如果同时有常规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 |
调优时,我一般遵循以下步骤:
- 先确认硬件能力:查看
/proc/asound/card0/pcm0p/sub0/hw_params,确认驱动支持的最小缓冲区大小。 - 从128帧开始:128帧在48kHz下约2.6ms,是个比较安全的起点。
- 逐步降低缓冲区:降到64帧(1.3ms),测试是否有XRun(缓冲区欠载或溢出)。
- 开启MMAP:如果硬件支持,切换到MMAP模式,延迟还能再降。
- 调整CPU亲和性:将FastMixer线程绑定到某个专用核心,避免被其他任务干扰。
避坑指南:我曾经把缓冲区降到32帧,延迟确实漂亮,但设备在播放复杂音效时频繁出现爆音。后来发现是DMA传输来不及填充数据。所以调优不能只看延迟,还要考虑系统的整体负载。
11.5 性能指标
衡量低延迟通路的好坏,主要看这几个指标:
- 端到端延迟:从App写入数据到扬声器发出声音的时间。通常用
dumpsys media.audio_flinger查看。 - XRun次数:缓冲区欠载(Underrun)或溢出(Overrun)的次数。理想情况是0。
- CPU占用率:低延迟通路虽然优先级高,但不能吃掉太多CPU资源。
- 抖动(Jitter):每次DMA传输的时间间隔是否稳定。抖动大,音质就会受影响。
我常用的测试工具是AudioFlinger的dumpsys和tinyplay/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 核心逻辑图
下面这张图展示了低延迟通路的整体架构和数据流向。我特意把常规通路也画出来做对比,这样更直观。
从图上可以清楚看到,FastMixer和FastCapture走的是绿色箭头,绕过了常规的MixerThread。MMAP通路更直接,App和驱动之间通过共享内存交互,连AudioFlinger的拷贝都省了。
好了,低延迟通路的核心内容就这些。说实话,这部分知识光看文档是不够的,一定要动手去调、去测。我建议你找一台设备,打开dumpsys,看看你的FastMixer跑在什么缓冲区大小下,有没有XRun。实践出真知。