音频策略定制:厂商定制接口、策略XML配置、动态策略加载、多产品线适配方案

音频策略定制,说白了就是让Android音频系统听懂你产品的「方言」。

我刚开始接触这块时,以为策略就是改改XML里的几个数字。后来才发现,真正的定制远不止这些——你要面对的是不同硬件平台、不同产品形态、甚至不同市场的合规要求。今天我就把这几年踩过的坑、总结的经验,一次性讲清楚。

一、厂商定制接口:系统留给你的「后门」

Android的音频策略框架,其实留了不少扩展点。我个人习惯把这些接口分成三类:

  • 策略引擎接口:AudioPolicyManager的派生与重写
  • 策略规则接口:动态策略规则的注入点
  • 策略配置接口:XML解析器的扩展点

先看最核心的——如何重写AudioPolicyManager。我记得第一次做这个时,翻源码翻到凌晨三点。

// 厂商自定义策略管理器
class VendorAudioPolicyManager : public AudioPolicyManager {
public:
    // 重写设备选择逻辑
    audio_devices_t getDeviceForStrategy(routing_strategy strategy, 
                                          bool fromCache) override {
        // 先走默认逻辑
        audio_devices_t device = AudioPolicyManager::getDeviceForStrategy(strategy, fromCache);
        
        // 再根据产品需求调整
        if (strategy == STRATEGY_MEDIA && isSpeakerMode()) {
            // 我遇到过某些平板产品,媒体播放必须强制走扬声器
            device = AUDIO_DEVICE_OUT_SPEAKER;
        }
        return device;
    }
    
    // 重写音量曲线
    status_t checkAndSetVolume(audio_stream_type_t stream, 
                                float volume, 
                                audio_io_handle_t output) override {
        // 定制音量映射逻辑
        float mappedVolume = mapVolumeForProduct(stream, volume);
        return AudioPolicyManager::checkAndSetVolume(stream, mappedVolume, output);
    }
};

这里有个关键点:不要直接修改AOSP源码。你想想看,每次Google发安全补丁,你都要重新merge,那得多痛苦?正确的做法是通过继承和组合,把定制逻辑隔离在厂商模块里。

我的经验:在Android.bp中把厂商策略模块编译成静态库,通过依赖注入的方式挂接到AudioFlinger。这样升级系统时,只需要重新编译厂商模块即可。

二、策略XML配置:从看懂到改对

audio_policy_configuration.xml,这个文件我看了不下百遍。它的结构其实很清晰:

配置层级 作用 常见定制点
globalConfiguration 全局策略开关 强制设备路由、音量曲线类型
modules 音频设备模块定义 增加新设备类型、修改采样率
devicePorts 物理设备端口 设备别名、地址映射
mixPorts 混音器端口 通道数、格式、采样率
routes 路由规则 设备间连接关系
profiles 音频Profile 编解码器配置、延迟参数

举个例子,我曾经需要为一个车载产品增加「后排娱乐」音频通道。做法是这样的:

<!-- 在devicePorts中增加新设备 -->
<devicePort tagName="Rear Entertainment" type="AUDIO_DEVICE_OUT_REAR" role="source">
    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
</devicePort>

<!-- 在routes中建立连接 -->
<route type="mix" sink="Rear Entertainment">
    <source src="mixport_rear"/>
</route>

嗯,这里要注意:XML中的设备类型必须与HAL层定义的枚举值一致。我见过有人把AUDIO_DEVICE_OUT_REAR写成了AUDIO_DEVICE_OUT_REAR_SEAT,结果设备死活不认。

三、动态策略加载:运行时切换的魔法

静态XML配置有个硬伤——产品运行时可能需要切换策略。比如手机插上游戏手柄,音频路由要变;车载从驾驶模式切换到休息模式,音效也要变。

Android 12之后,系统提供了动态策略加载机制。核心思路是:

  1. 定义多个策略配置文件,放在/vendor/etc/audio_policy/目录下
  2. 通过属性或服务触发策略切换
  3. AudioPolicyManager重新解析配置并生效

我参与过一个项目,需要根据蓝牙设备类型切换策略。代码大致是这样:

// 动态加载策略
void AudioPolicyManager::loadDynamicPolicy(const char* policyName) {
    // 构建配置文件路径
    char path[256];
    snprintf(path, sizeof(path), "/vendor/etc/audio_policy/%s.xml", policyName);
    
    // 解析新配置
    sp<AudioPolicyConfig> newConfig = new AudioPolicyConfig();
    status_t status = newConfig->loadFromXml(path);
    
    if (status == NO_ERROR) {
        // 替换当前配置
        Mutex::Autolock _l(mLock);
        mConfig = newConfig;
        
        // 重新初始化路由
        initializeDeviceSelection();
        
        // 通知HAL层更新
        mpClientInterface->setParameters(AUDIO_PARAMETER_KEY_POLICY_CHANGED, "true");
    }
}
避坑指南:动态切换策略时,正在播放的音频流可能会短暂中断。我曾经因为这个被测试团队追着改了三版。解决方案是:在切换前先暂停非关键音频流,等策略生效后再恢复。

四、多产品线适配方案:一套代码,多款产品

大厂通常有多个产品线——手机、平板、车载、IoT。每个产品的音频需求都不一样。怎么用一套代码搞定?

我总结了一套「三层适配」方案:

  • 第一层:产品标识层——通过ro.product.audio.features属性区分产品
  • 第二层:策略配置层——每个产品有自己的XML配置目录
  • 第三层:运行时适配层——根据产品标识动态加载不同策略

目录结构建议这样组织:

/vendor/etc/audio_policy/
├── common/                    # 公共配置
│   ├── audio_policy_configuration_base.xml
│   └── audio_policy_volumes.xml
├── phone/                     # 手机产品线
│   ├── audio_policy_configuration.xml
│   └── audio_policy_engine.xml
├── tablet/                    # 平板产品线
│   ├── audio_policy_configuration.xml
│   └── audio_policy_engine.xml
└── vehicle/                   # 车载产品线
    ├── audio_policy_configuration.xml
    ├── audio_policy_engine.xml
    └── audio_policy_routing.xml

在代码中,通过读取产品属性来决定加载哪个目录:

// 获取产品音频特性
const char* productFeature = property_get("ro.product.audio.features", "default");

// 根据特性选择策略目录
const char* policyDir;
if (strcmp(productFeature, "phone") == 0) {
    policyDir = "/vendor/etc/audio_policy/phone/";
} else if (strcmp(productFeature, "tablet") == 0) {
    policyDir = "/vendor/etc/audio_policy/tablet/";
} else if (strcmp(productFeature, "vehicle") == 0) {
    policyDir = "/vendor/etc/audio_policy/vehicle/";
} else {
    policyDir = "/vendor/etc/audio_policy/common/";
}

// 加载策略
loadPolicyFromDirectory(policyDir);
核心原则:公共逻辑放common目录,产品差异通过XML配置体现。代码层尽量不做产品判断,把决策权交给配置层。这样新增产品线时,只需要写XML,不需要改C++代码。

五、知识体系总览

下面这张图,是我梳理的音频策略定制整体架构。你看完应该能有个全局认识:

音频策略定制知识体系 厂商定制接口 AudioPolicyManager派生 策略规则注入点 策略XML配置 devicePorts/routes mixPorts/profiles 动态策略加载 运行时切换配置 属性触发/服务调用 AudioPolicyManager 核心引擎 设备选择 | 音量管理 | 路由决策 | 策略切换 多产品线适配方案 产品标识层 ro.product.audio.features 策略配置层 产品独立XML目录 运行时适配层 动态加载与切换 HAL层适配 设备驱动对接

这张图把整个知识体系串起来了。从上到下,从接口到配置,再到运行时和多产品线适配。你想想看,是不是每个环节都有对应的定制手段?

我的建议:刚开始做音频策略定制时,先从XML配置入手。改一个设备路由,看看效果,再逐步深入到代码层。别一上来就动AudioPolicyManager,容易翻车。

音频策略定制,说到底就是「理解规则、利用规则、扩展规则」。系统给的接口和配置方式,足够覆盖绝大多数产品需求。关键是要理清哪些该改配置,哪些该改代码,哪些该留给运行时决策。

好了,这一章的内容就到这里。希望这些实战经验能帮你少走弯路。


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