14、音频驱动开发:QNX音频架构(Audio Manager)、Android Audio HAL与AudioFlinger、麦克风/扬声器驱动、回声消除与降噪策略

音频驱动,在智能座舱里是个“看似简单,实则坑多”的模块。你想想看,用户对车机最直接的感知,除了屏幕亮不亮,就是声音好不好。导航播报、电话通话、语音助手、媒体播放……哪一样离得开音频?

我个人习惯把音频驱动分成三层来看:底层是麦克风和扬声器的硬件驱动,中间是QNX或Android的音频框架,上层则是策略和算法。今天我们就一层层剥开它。

车载音频驱动架构总览 应用层:导航 / 电话 / 语音助手 / 媒体播放 框架层 QNX Audio Manager Android AudioFlinger + Audio HAL 驱动层:I2S / PCM / TDM 控制器驱动 + Codec 驱动 硬件层:麦克风阵列 / 扬声器 / 音频Codec芯片 策略与路由 硬件抽象

14.1 QNX 音频架构:Audio Manager 到底管什么?

QNX 的音频架构,核心就是 Audio Manager。它不直接操作硬件,而是做策略和路由。说白了,它决定“导航声音该走哪个扬声器”、“电话进来时媒体要不要暂停”。

我在项目中遇到过一个问题:导航播报时,音乐只是被压低,而不是完全暂停。客户要求必须暂停。结果发现是 Audio Manager 的策略配置里,把导航和媒体的 stream 类型设成了同一个优先级组。改一下策略文件就解决了。

QNX 的音频流通常用 snd_pcm_open() 来打开设备节点。设备节点在 /dev/snd/ 下,比如 pcmC0D0c 表示 card0 device0 capture(录音),pcmC0D0p 表示 playback(播放)。

核心概念: QNX 的 Audio Manager 通过 ioctl 与底层驱动通信,管理音频路由、音量、采样率转换等。它不负责编解码,那是上层应用的事。

14.2 Android Audio HAL 与 AudioFlinger

Android 的音频架构,比 QNX 要复杂一些。核心有两个东西:AudioFlingerAudio HAL

AudioFlinger 是 Android 的音频服务,跑在 mediaserver 进程里。它负责混音、路由、策略执行。而 Audio HAL 是硬件抽象层,它定义了 AudioFlinger 如何与底层驱动交互。

你想想看,AudioFlinger 要播放一段音频,它会调用 HAL 的 out_write() 函数。这个函数由芯片厂商实现,比如高通、MTK、NXP 都有自己的实现。HAL 层再往下调用内核驱动,最终把数据送到 Codec 芯片。

我记得有一次调试车载蓝牙通话,声音断断续续。查了半天,发现是 AudioFlinger 的 buffer size 配置太小,导致底层驱动频繁中断。把 config_audio_policy.xml 里的 buffer_size 从 256 改成 512,问题就消失了。

调试技巧: 在 Android 上,可以用 dumpsys media.audio_flinger 查看当前音频流的状态,包括采样率、通道数、buffer 大小。这个命令在车载调试中非常实用。

14.3 麦克风与扬声器驱动:I2S 和 TDM

麦克风和扬声器的驱动,本质上就是操作 I2S 或 TDM 总线。I2S 是标准的数字音频接口,有三根线:时钟(BCLK)、帧同步(LRCK)、数据(SDATA)。

TDM 是 I2S 的变种,可以在一根数据线上传输多个通道。车载座舱里经常用 TDM,因为麦克风阵列可能有 4 个、6 个甚至 8 个通道,用 TDM 一根线就能搞定。

驱动开发的核心工作,就是配置 DMA 传输。音频数据是流式的,必须用 DMA 来搬运,否则 CPU 会被中断淹没。我习惯在 probe 函数里先申请 DMA buffer,然后注册一个 snd_soc_dai_driver,把 playback 和 capture 的 ops 填好。

// 一个简化的 I2S 驱动片段
static int my_i2s_hw_params(struct snd_pcm_substream *substream,
                             struct snd_pcm_hw_params *params,
                             struct snd_soc_dai *dai)
{
    struct my_i2s_dev *dev = snd_soc_dai_get_drvdata(dai);
    unsigned int rate = params_rate(params);
    unsigned int channels = params_channels(params);

    // 配置采样率和通道数
    dev->bclk = rate * channels * 32; // 32位数据
    dev->lrck = rate;

    // 配置 DMA 传输
    snd_dmaengine_pcm_set_config_from_dai_data(substream,
                                                snd_soc_dai_get_dma_data(dai, substream));

    return 0;
}

注意: 车载环境对音频时钟要求很高。我曾经遇到过因为晶振精度不够,导致 I2S 的 BCLK 抖动,播放出来的声音有“嘶嘶”声。后来换了一个低抖动的晶振才解决。所以选型时一定要看 Codec 芯片的时钟源要求。

14.4 回声消除与降噪策略

回声消除(AEC)和降噪(ANC),是车载音频里最头疼的问题。你想想看,车里空间小,扬声器离麦克风近,如果不做回声消除,打电话时对方会听到自己的回声。

回声消除的原理,说白了就是“预测+抵消”。系统会从扬声器播放的参考信号中,预测出麦克风会收到多少回声,然后从麦克风信号中减去这部分。算法上常用 NLMS(归一化最小均方)或频域自适应滤波。

降噪则分两种:被动降噪和主动降噪。被动降噪靠物理结构,比如麦克风防风罩。主动降噪靠算法,比如谱减法或维纳滤波。

在驱动层面,我们需要做的是:

  • 确保麦克风和扬声器的采样率同步,否则 AEC 算法会失效。
  • 提供低延迟的音频路径。AEC 算法对延迟敏感,一般要求端到端延迟小于 20ms。
  • 在 HAL 层暴露 AEC 的参考信号接口。Android 的 AudioRecordAudioTrack 可以通过 AudioSystem::getInputForAttr() 获取参考流。

我曾在某个项目中,AEC 效果一直不理想。后来发现是麦克风驱动里用了软件重采样,引入了额外的延迟。把重采样去掉,改用硬件支持的采样率,延迟从 30ms 降到了 12ms,回声消除效果立竿见影。

经验之谈: 回声消除和降噪,70% 靠算法,30% 靠驱动。驱动做得好,算法事半功倍。驱动做得差,再牛的算法也救不回来。尤其是延迟和时钟同步,这两点是硬伤。

14.5 实战中的避坑指南

做音频驱动开发,有几个坑我踩过,分享给你:

  • DMA buffer 大小: 太小会导致 underrun(播放时数据跟不上),太大则延迟高。我一般从 1024 帧开始调,根据实际效果增减。
  • 电源管理: 音频 Codec 在休眠后重新唤醒,有时会丢失配置。记得在 resume 回调里重新初始化 Codec 寄存器。
  • 多路音频并发: 车载场景下,导航、电话、媒体可能同时播放。AudioFlinger 的混音策略要提前规划好,否则会出现“抢通道”的问题。
  • 调试手段:tinymix 查看 Codec 的 mixer 控件,用 tinycap 录音,用 tinyplay 播放。这些工具在 QNX 和 Android 上都有移植版本。

嗯,音频驱动开发就是这样,细节决定成败。从 Audio Manager 的策略配置,到 I2S 总线的时钟设置,再到 AEC 算法的延迟优化,每一步都需要你沉下心来去调。没有捷径,但每解决一个问题,你都会对这套系统有更深的理解。


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