Android 15音频子系统(五):AudioPolicyService策略管理深度解析

一、引言:谁来决定声音该从哪里出来?

假设你正在手机上用耳机听音乐,突然来了一个电话。你接通电话后,声音应该从扬声器还是耳机里出来?听筒还是耳机扬声器?如果用户同时连着蓝牙耳机和有线耳机,又该怎么选?

这些问题看似简单,实则暗藏玄机。在 Android 系统里,负责做这类决策的就是今天的主角------AudioPolicyService(音频策略服务)。

前几篇我们介绍了 AudioFlinger 的混音机制------它就像一个全力运转的"音频工厂",负责把音频数据混合、处理后送给硬件。但工厂只管生产,不管销售策略。AudioPolicyService 就是那个"销售总监":它不碰音频数据,专门负责决策音频应该走哪条路。

这种职责分离的设计非常优雅:AudioFlinger 关注怎么播放 ,AudioPolicyService 关注往哪里播放。本篇将深入 AudioPolicyService 的内部,看看 Android 15 是如何处理这些复杂的路由策略的。

二、AudioPolicyService 架构概览

2.1 在系统中的位置

AudioPolicyService(APS)运行在 audioserver 进程中,和 AudioFlinger 是"邻居"。它们都由 audioserver.rc 启动,共享同一进程空间,但通过独立的 Binder 服务对外暴露接口。

arduino 复制代码
// frameworks/av/media/audioserver/main_audioserver.cpp
int main(int argc __unused, char **argv)
{
    // ...
    AudioFlinger::instantiate();        // 音频混音引擎
    AudioPolicyService::instantiate();  // 音频策略服务
    // ...
}

从架构图可以看出 APS 在整个音频系统中的位置:

系统中有两个与音频策略相关的服务容易混淆:

  • AudioService (Java 层,运行在 system_server):提供 AudioManager API,是应用访问的门户
  • AudioPolicyService (Native 层,运行在 audioserver):真正执行策略决策的引擎

应用调用 AudioManager.setMode() 时,最终会跨进程 Binder 调用到 APS 的 setPhoneState()。

2.2 核心组件层次

APS 内部的层次结构如下:

scss 复制代码
AudioPolicyService
├── AudioPolicyManager          // 策略管理主体,处理大部分请求
│   ├── Engine (EngineInterface) // 路由决策引擎
│   │   └── EngineBase          // 默认引擎实现
│   ├── AudioPolicyMix           // Mix策略管理(动态路由)
│   └── HwModule                 // 硬件模块抽象
└── AudioCommandThread           // 异步命令线程

AudioPolicyManager(APM) 是整个策略逻辑的核心,它持有当前系统的全局状态:已连接的设备列表、已打开的输入输出流、活跃的音频策略等。

Engine 是路由决策的大脑,从 APM 的庞大逻辑中单独抽出,通过 EngineInterface 接口隔离。Engine 的默认实现读取 audio_policy_engine_configuration.xml,根据 Mode、ForceUse 等条件计算最终设备选择。

AudioCommandThread 负责异步处理设备连接/断开等耗时操作,避免阻塞调用方。

2.3 与 AudioFlinger 的协作

两者之间的交互简洁而高效:

scss 复制代码
App                 AudioPolicyService          AudioFlinger
 |                        |                          |
 |-- getOutput() -------> |                          |
 |                   决策路由...                      |
 |                        |---- openOutput() ------> |
 |                        |<--- output handle ------- |
 |<-- IoHandle ---------- |                          |
 |                                                   |
 |-- createTrack(IoHandle) -----------------------> |

APS 只负责决定用哪个输出流(IoHandle),AudioFlinger 负责在这个流上实际创建 Track 并处理音频数据。策略和执行完全解耦。

三、策略配置文件:audio_policy_configuration.xml

3.1 文件位置与加载时机

Android 的音频路由拓扑由 XML 配置文件定义,位置因厂商而异:

bash 复制代码
# 主配置文件(设备商提供)
/vendor/etc/audio_policy_configuration.xml

# AOSP默认配置(通用参考)
/system/etc/audio_policy_configuration.xml

# 引擎配置(策略规则)
/system/etc/audio_policy_engine_configuration.xml

AudioPolicyManager 初始化时解析这些文件,构建设备拓扑图。

3.2 配置文件结构解析

一个典型的配置文件结构:

xml 复制代码
<!-- audio_policy_configuration.xml 核心结构 -->
<audioPolicyConfiguration version="1.0">
    <globalConfiguration speaker_drc_enabled="true"/>

    <modules>
        <!-- 主音频模块(通常对应 audio.primary.so HAL) -->
        <module name="primary" halVersion="2.0">

            <!-- mixPort: 代表 AudioFlinger 中的一个音频线程 -->
            <mixPorts>
                <mixPort name="primary output" role="source"
                         flags="AUDIO_OUTPUT_FLAG_PRIMARY">
                    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
                </mixPort>

                <mixPort name="deep_buffer" role="source"
                         flags="AUDIO_OUTPUT_FLAG_DEEP_BUFFER">
                    <profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
                             samplingRates="48000" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
                </mixPort>
            </mixPorts>

            <!-- devicePort: 代表物理音频设备 -->
            <devicePorts>
                <devicePort tagName="Speaker" type="AUDIO_DEVICE_OUT_SPEAKER"
                            role="sink">
                    <gains>
                        <gain name="" mode="AUDIO_GAIN_MODE_JOINT"
                              minValueMB="-9600" maxValueMB="0" defaultValueMB="0"
                              stepValueMB="100"/>
                    </gains>
                </devicePort>
                <devicePort tagName="Earpiece" type="AUDIO_DEVICE_OUT_EARPIECE" role="sink"/>
                <devicePort tagName="Wired Headset" type="AUDIO_DEVICE_OUT_WIRED_HEADSET" role="sink"/>
                <devicePort tagName="BT A2DP" type="AUDIO_DEVICE_OUT_BLUETOOTH_A2DP" role="sink"/>
                <devicePort tagName="BT SCO" type="AUDIO_DEVICE_OUT_BLUETOOTH_SCO" role="sink"/>
            </devicePorts>

            <!-- routes: 定义 mixPort 到 devicePort 的连接关系 -->
            <routes>
                <route type="mix" sink="Speaker" sources="primary output,deep_buffer"/>
                <route type="mix" sink="Wired Headset" sources="primary output,deep_buffer"/>
                <route type="mix" sink="BT A2DP" sources="primary output,deep_buffer"/>
            </routes>
        </module>
    </modules>
</audioPolicyConfiguration>

三个核心概念:

  • mixPort:对应 AudioFlinger 中的一个 PlaybackThread,是软件端口
  • devicePort:对应实际的物理硬件设备(扬声器、耳机插孔、蓝牙等)
  • route:定义哪些 mixPort 的音频可以路由到哪个 devicePort

厂商在适配新硬件时,主要工作就是修改这个配置文件,添加新的 devicePort 并配置相应的 route。

3.3 引擎配置:策略规则定义

与拓扑配置不同,audio_policy_engine_configuration.xml 定义的是业务规则:

xml 复制代码
<!-- 定义 product strategy(产品策略) -->
<ProductStrategies>
    <ProductStrategy name="STRATEGY_MEDIA">
        <AttributesGroup streamType="AUDIO_STREAM_MUSIC" volumeGroup="music">
            <Attributes usage="AUDIO_USAGE_MEDIA"/>
            <Attributes usage="AUDIO_USAGE_GAME"/>
        </AttributesGroup>
        <!-- 设备优先级列表:按优先级从高到低 -->
        <DeviceConfiguration>
            <Device type="AUDIO_DEVICE_OUT_WIRED_HEADPHONE"/>
            <Device type="AUDIO_DEVICE_OUT_WIRED_HEADSET"/>
            <Device type="AUDIO_DEVICE_OUT_BLUETOOTH_A2DP"/>
            <Device type="AUDIO_DEVICE_OUT_USB_DEVICE"/>
            <Device type="AUDIO_DEVICE_OUT_SPEAKER"/>
        </DeviceConfiguration>
    </ProductStrategy>

    <ProductStrategy name="STRATEGY_PHONE">
        <AttributesGroup streamType="AUDIO_STREAM_VOICE_CALL">
            <Attributes usage="AUDIO_USAGE_VOICE_COMMUNICATION"/>
        </AttributesGroup>
        <DeviceConfiguration>
            <!-- 通话场景:蓝牙免提 > 有线耳机 > 听筒 -->
            <Device type="AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET"/>
            <Device type="AUDIO_DEVICE_OUT_WIRED_HEADSET"/>
            <Device type="AUDIO_DEVICE_OUT_EARPIECE"/>
        </DeviceConfiguration>
    </ProductStrategy>
</ProductStrategies>

这就是为什么"插上有线耳机音乐会自动切换到耳机"------因为 STRATEGY_MEDIA 的设备优先级列表里,有线耳机排在扬声器前面。

四、Stream 类型与策略映射

4.1 Legacy Stream 与 AudioAttributes

Android 早期只有简单的 stream type:STREAM_MUSIC、STREAM_RING、STREAM_ALARM 等。随着场景增多,Android 5.0 引入了 AudioAttributes,提供更精细的语义描述。

两者之间存在明确的映射关系:

Stream Type AudioAttributes Usage 典型场景
STREAM_MUSIC USAGE_MEDIA / USAGE_GAME 音乐播放、游戏音效
STREAM_VOICE_CALL USAGE_VOICE_COMMUNICATION 电话通话
STREAM_RING USAGE_NOTIFICATION_RINGTONE 来电铃声
STREAM_ALARM USAGE_ALARM 闹钟
STREAM_NOTIFICATION USAGE_NOTIFICATION 消息通知
STREAM_SYSTEM USAGE_ASSISTANCE_SONIFICATION 按键音、触控反馈
STREAM_DTMF USAGE_VOICE_COMMUNICATION_SIGNALLING 拨号按键音

4.2 策略层的映射链

从应用的 AudioAttributes 到最终设备,经历以下映射:

arduino 复制代码
AudioAttributes (usage=USAGE_MEDIA)
    ↓ Engine::getProductStrategyForAttributes()
ProductStrategy (STRATEGY_MEDIA)
    ↓ Engine::getDevicesForProductStrategy()
DevicePriorityList [HEADPHONE > HEADSET > BT_A2DP > USB > SPEAKER]
    ↓ 过滤当前可用设备
FinalDevice (DEVICE_OUT_WIRED_HEADSET)  // 例:有线耳机已插入

关键源码在 EngineBase::getDevicesForProductStrategy():

cpp 复制代码
// frameworks/av/services/audiopolicy/enginedefault/src/EngineBase.cpp
DeviceVector EngineBase::getDevicesForProductStrategy(
        product_strategy_t strategy) const
{
    const auto& strategyDevices = getProductStrategies().getDevicesForProductStrategy(strategy);

    // 从优先级列表中过滤出当前可用的设备
    DeviceVector availableDevices = getApmObserver()->getAvailableOutputDevices();

    for (const auto& device : strategyDevices) {
        if (availableDevices.contains(device)) {
            return DeviceVector(device);  // 返回优先级最高的可用设备
        }
    }
    return DeviceVector();
}

逻辑清晰:遍历优先级列表,返回第一个在当前可用设备中存在的设备。

五、音频路由决策全流程

当应用调用 AudioTrack.Builder().build() 时,内部会触发一次完整的路由决策流程:

5.1 getOutputForAttr():路由决策入口

cpp 复制代码
// frameworks/av/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
status_t AudioPolicyManager::getOutputForAttr(
        const audio_attributes_t *attr,
        audio_io_handle_t *output,
        audio_session_t session,
        audio_stream_type_t *stream,
        uid_t uid,
        const audio_config_t *config,
        audio_output_flags_t *flags,
        audio_port_handle_t *selectedDeviceId,
        audio_port_handle_t *portId,
        std::vector<audio_io_handle_t> *secondaryOutputs,
        output_type_t *outputType,
        bool *isSpatialized,
        bool *isBitPerfect)
{
    // 1. AudioAttributes 转 ProductStrategy
    result_device_t resultDevice = getDeviceAndMixForAttributes(*attr, uid, ...);

    // 2. 通过 Engine 决策输出设备
    sp<DeviceDescriptor> outputDevice = resultDevice.device;

    // 3. 选择合适的输出流(matchFlags + 设备匹配)
    *output = getOutputForDevice(outputDevice, session, *stream,
                                  config, flags, ...);
    return NO_ERROR;
}

5.2 Engine 的综合决策

Engine 在决策时考虑多个维度:

cpp 复制代码
// frameworks/av/services/audiopolicy/enginedefault/src/Engine.cpp
DeviceVector Engine::getOutputDevicesForAttributes(
        const audio_attributes_t &attributes,
        const sp<DeviceDescriptor> preferredDevice,
        bool fromCache) const
{
    // 优先使用指定设备(应用层强制路由)
    if (preferredDevice != nullptr) {
        return DeviceVector(preferredDevice);
    }

    // 获取产品策略
    auto strategy = getProductStrategyForAttributes(attributes);

    // IN_CALL 模式:强制走通话路由
    if (mPhoneState == AUDIO_MODE_IN_CALL) {
        strategy = getProductStrategyForStream(AUDIO_STREAM_VOICE_CALL);
    }

    return getDevicesForProductStrategy(strategy);
}

三个决策因素的优先级:

  1. 应用指定设备 (setPreferredDevice()):优先级最高
  2. 音频模式(Mode) :IN_CALL 会强制走通话路由
  3. 策略优先级列表:正常情况下按设备优先级选择

5.3 openOutput:把决策落地

找到目标设备后,APM 调用 getOutputForDevice() 选择或创建对应的输出流:

cpp 复制代码
audio_io_handle_t AudioPolicyManager::getOutputForDevice(
        const sp<DeviceDescriptor>& device,
        audio_session_t session,
        audio_stream_type_t stream,
        const audio_config_t *config,
        audio_output_flags_t *flags)
{
    // 在已有的输出流中找到可复用的
    for (size_t i = 0; i < mOutputs.size(); i++) {
        sp<SwAudioOutputDescriptor> desc = mOutputs.valueAt(i);
        if (desc->supportedDevices().contains(device) &&
            desc->isCompatibleProfile(...)) {
            return desc->mIoHandle;  // 复用已有输出流
        }
    }

    // 没有合适的,请求 AudioFlinger 打开新输出流
    audio_io_handle_t output = AUDIO_IO_HANDLE_NONE;
    status_t status = mpClientInterface->openOutput(
            profile->getModuleHandle(), &output, &outputConfig, device, ...);

    return output;
}

六、设备管理:连接、断开与优先级

6.1 设备连接通知

当用户插入有线耳机,内核驱动检测到插入事件,最终通过以下路径通知到 APM:

arduino 复制代码
内核 jack detection
    ↓ AudioHAL (setParameters "headset_connect=true")
    ↓ AudioFlinger
    ↓ AudioPolicyClient::setAudioPortConfig()
    ↓ AudioPolicyManager::setDeviceConnectionState()

APM 收到连接通知后做三件事:

cpp 复制代码
// frameworks/av/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
status_t AudioPolicyManager::setDeviceConnectionState(
        const sp<DeviceDescriptor> &device,
        audio_policy_dev_state_t state, ...)
{
    if (state == AUDIO_POLICY_DEVICE_STATE_AVAILABLE) {
        // 1. 更新可用设备列表
        mAvailableOutputDevices.add(device);

        // 2. 异步通知 AudioFlinger 更新路由
        checkOutputsForDevice(device, state, outputs);

        // 3. 触发路由重新计算
        updateCallAndOutputRouting();
    }
    // ...
}

6.2 设备断开的 Failover 机制

设备断开时,APM 会自动执行 Failover------找下一个优先级最高的可用设备:

cpp 复制代码
void AudioPolicyManager::checkOutputsForDevice(
        const sp<DeviceDescriptor>& device,
        audio_policy_dev_state_t state,
        SortedVector<audio_io_handle_t>& outputs)
{
    if (state == AUDIO_POLICY_DEVICE_STATE_UNAVAILABLE) {
        // 设备断开:关闭使用该设备的输出流,触发重新路由
        for (auto output : outputs) {
            sp<SwAudioOutputDescriptor> desc = mOutputs.valueFor(output);
            if (desc->devices().contains(device)) {
                // 切换到下一个优先级设备
                DeviceVector newDevices = getNewOutputDevices(desc, false);
                if (newDevices != desc->devices()) {
                    setOutputDevices(desc, newDevices, true);
                }
            }
        }
    }
}

这就是"拔出耳机音乐自动切回扬声器"的实现原理------不是魔法,是 Failover。

6.3 BT A2DP 的特殊处理

蓝牙 A2DP 设备的连接/断开比有线设备复杂,因为涉及蓝牙协议栈的异步状态:

cpp 复制代码
// 蓝牙设备连接时,需要等待 A2DP profile 建立完成
status_t AudioPolicyManager::setDeviceConnectionStateInt(
        audio_devices_t deviceType,
        audio_policy_dev_state_t state, ...)
{
    if (audio_is_a2dp_out_device(deviceType)) {
        // A2DP 连接是异步的,通过 BtA2dpDeviceStatus 跟踪状态
        if (state == AUDIO_POLICY_DEVICE_STATE_AVAILABLE) {
            // 设置延迟生效:等待 A2DP 编解码协商完成
            mA2dpSuspended = false;
        }
    }
    // ...
}

蓝牙设备有独立的 A2dpSuspended 状态,在通话中会被挂起(切回听筒/有线耳机),通话结束后自动恢复。

七、音频模式切换

7.1 四种音频模式

Android 定义了四种音频模式,影响系统整体的路由策略:

Mode 值 触发条件 效果
AUDIO_MODE_NORMAL 0 默认状态 按各策略优先级路由
AUDIO_MODE_RINGTONE 1 来电铃声 铃声走 RINGTONE 策略
AUDIO_MODE_IN_CALL 2 接通电话 强制走 PHONE 策略,启用 EC/NS
AUDIO_MODE_IN_COMMUNICATION 3 VoIP 通话 类似 IN_CALL,但走软件路径

7.2 setPhoneState() 触发路由重计算

当 TelecomService 检测到通话状态变化时,会调用:

cpp 复制代码
// frameworks/av/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
void AudioPolicyManager::setPhoneState(audio_mode_t state)
{
    audio_mode_t oldState = mPhoneState;
    mPhoneState = state;

    // 通知 Engine 更新模式
    mEngine->setPhoneState(state);

    // 进入通话:关闭蓝牙 A2DP,激活 SCO
    if (isStateInCall(state)) {
        if (!isStateInCall(oldState)) {
            // NORMAL -> IN_CALL: 挂起所有媒体输出
            for (auto output : mOutputs) {
                if (isMediaOutput(output)) {
                    setStrategyMute(STRATEGY_MEDIA, true, output);
                }
            }
        }
    }

    // 触发通话输入输出路由更新
    updateCallAndOutputRouting();
}

进入 IN_CALL 模式后,即使应用还在播放音乐,STRATEGY_MEDIA 的音频也会被 mute,通话声音独占通话路径。

7.3 EC/NS 的自动启停

进入 IN_CALL 和 IN_COMMUNICATION 模式时,APM 还会自动在输入链路上挂载回声消除(EC)和噪声抑制(NS)效果器,这是通话质量的基本保障。

八、ForceUse:我说了算

8.1 什么是 ForceUse

ForceUse 是一种策略覆盖机制,允许特定场景强制指定使用某类设备。它是系统级功能,普通 App 无法调用(需要 MODIFY_AUDIO_ROUTING 权限)。

cpp 复制代码
// 强制使用场景定义
enum audio_policy_force_use {
    AUDIO_POLICY_FORCE_FOR_COMMUNICATION,   // 通话设备
    AUDIO_POLICY_FORCE_FOR_MEDIA,           // 媒体输出
    AUDIO_POLICY_FORCE_FOR_RECORD,          // 录音输入
    AUDIO_POLICY_FORCE_FOR_DOCK,            // 底座
    AUDIO_POLICY_FORCE_FOR_SYSTEM,          // 系统音效
    AUDIO_POLICY_FORCE_FOR_HDMI_SYSTEM_AUDIO, // HDMI
    AUDIO_POLICY_FORCE_FOR_ENCODED_SURROUND,  // 环绕声
    AUDIO_POLICY_FORCE_FOR_VIBRATE_RINGING,   // 振动铃声
};

// 强制使用的设备
enum audio_policy_forced_cfg {
    AUDIO_POLICY_FORCE_NONE,          // 不强制(默认)
    AUDIO_POLICY_FORCE_SPEAKER,       // 强制扬声器
    AUDIO_POLICY_FORCE_HEADPHONES,    // 强制耳机
    AUDIO_POLICY_FORCE_BT_SCO,        // 强制蓝牙通话
    AUDIO_POLICY_FORCE_BT_A2DP,       // 强制蓝牙音乐
    AUDIO_POLICY_FORCE_WIRED_ACCESSORY, // 强制有线设备
    AUDIO_POLICY_FORCE_BT_CAR_DOCK,   // 强制车载底座
    AUDIO_POLICY_FORCE_DESK_DOCK,     // 强制桌面底座
    AUDIO_POLICY_FORCE_ANALOG_DOCK,   // 强制模拟底座
    AUDIO_POLICY_FORCE_DIGITAL_DOCK,  // 强制数字底座
    AUDIO_POLICY_FORCE_NO_BT_A2DP,    // 禁用蓝牙A2DP
    AUDIO_POLICY_FORCE_SYSTEM_ENFORCED, // 系统强制
    AUDIO_POLICY_FORCE_HDMI_SYSTEM_AUDIO_ENFORCED, // HDMI强制
    AUDIO_POLICY_FORCE_ENCODED_SURROUND_NEVER,  // 禁用环绕声
    AUDIO_POLICY_FORCE_ENCODED_SURROUND_ALWAYS, // 强制环绕声
    AUDIO_POLICY_FORCE_ENCODED_SURROUND_MANUAL, // 手动设置环绕声
};

8.2 典型场景:免提电话

"免提"按钮的实现就是 ForceUse:

java 复制代码
// frameworks/base/media/java/android/media/AudioManager.java
public void setSpeakerphoneOn(boolean on) {
    final IAudioService service = getService();
    service.setForceUse(
        AudioSystem.FOR_COMMUNICATION,
        on ? AudioSystem.FORCE_SPEAKER : AudioSystem.FORCE_NONE);
}

这个调用会传递到 APS,修改 AUDIO_POLICY_FORCE_FOR_COMMUNICATION 对应的强制配置。Engine 在下次路由决策时,会检查 ForceUse 配置,在通话场景强制选择扬声器。

8.3 典型场景:禁用蓝牙 A2DP

当用户在开发者选项中"禁用绝对音量"时,实际上触发的是:

java 复制代码
// 禁用 BT A2DP 自动切换
AudioSystem.setForceUse(AudioSystem.FOR_MEDIA, AudioSystem.FORCE_NO_BT_A2DP);

这会让 Engine 在媒体路由决策时跳过所有蓝牙 A2DP 设备,即使蓝牙耳机已连接也不会自动切换。

九、Android 15 的新变化

9.1 AIDL 全面替代 HIDL

Android 15 完成了音频 HAL 接口从 HIDL(HAL Interface Definition Language)到 AIDL 的迁移。这是一个影响深远的底层变化:

HIDL(Android 8-14):

scss 复制代码
// hardware/interfaces/audio/2.0/IDevice.hal
interface IDevice {
    setParameters(vec<ParameterValue> context, vec<ParameterValue> parameters)
        generates (Result retval);
};

AIDL(Android 15+):

csharp 复制代码
// hardware/interfaces/audio/aidl/android/hardware/audio/core/IModule.aidl
interface IModule {
    AudioPatch setAudioPatch(in AudioPatch requested);
    void resetAudioPatch(int patchId);
    void setAudioPortConfig(in AudioPortConfig requested);
}

AIDL 接口更加类型安全,支持并行处理,且与 Java/Kotlin 集成更好。

9.2 音量组(Volume Group)的增强

Android 15 在 Volume Group 管理上做了重要改进。Volume Group 是一组共享相同音量策略的音频流:

xml 复制代码
<!-- audio_policy_engine_configuration.xml Android 15 新增字段 -->
<VolumeGroups>
    <VolumeGroup name="music" indexMin="0" indexMax="15">
        <Volume deviceCategory="DEVICE_CATEGORY_SPEAKER" ref="DEFAULT_MEDIA_VOLUME_CURVE"/>
        <Volume deviceCategory="DEVICE_CATEGORY_HEADSET" ref="DEFAULT_MEDIA_VOLUME_CURVE"/>
    </VolumeGroup>

    <!-- Android 15 新增:空间音频音量组 -->
    <VolumeGroup name="spatialized" indexMin="0" indexMax="15">
        <Volume deviceCategory="DEVICE_CATEGORY_HEADSET" ref="SPATIALIZED_VOLUME_CURVE"/>
    </VolumeGroup>
</VolumeGroups>

9.3 空间音频路由策略

Android 15 增加了对空间音频(Spatialized Audio)的原生路由支持:

cpp 复制代码
// frameworks/av/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
bool AudioPolicyManager::canBeSpatialized(
        const audio_attributes_t *attr,
        const audio_config_t *config,
        const AudioDeviceTypeAddrVector &devices)
{
    // 仅支持头戴式设备(耳机)的空间化
    for (const auto& device : devices) {
        if (!isHeadTrackingSupportedForDevice(device)) {
            return false;
        }
    }

    // 检查音频格式支持(需要立体声以上)
    if (config->channel_mask != AUDIO_CHANNEL_OUT_STEREO &&
        !audio_is_channel_mask_spatialized(config->channel_mask)) {
        return false;
    }

    return mSpatializerOutput != AUDIO_IO_HANDLE_NONE;
}

空间音频输出走独立的 SpatializerOutput,避免与普通输出混合时精度损失。

9.4 AudioRecord 权限策略的变化

Android 15 加强了麦克风访问控制,APS 在 getInputForAttr() 中增加了更严格的权限检查:

cpp 复制代码
// Android 15: 新增敏感麦克风使用审计
status_t AudioPolicyManager::getInputForAttr(
        const audio_attributes_t *attr,
        audio_io_handle_t *input, ...)
{
    // 后台应用使用麦克风需要额外权限(Android 14已有)
    // Android 15新增:记录精确的麦克风使用时间戳用于隐私报告
    if (mAudioPolicyMix.is_background_capture(uid)) {
        halInputSource = AUDIO_SOURCE_REMOTE_SUBMIX;
    }
    // ...
}

十、实战案例

10.1 案例一:监听设备连接状态变化

App 如何知道用户插入或拔出了耳机?

kotlin 复制代码
class AudioDeviceListener(private val context: Context) {

    private val audioManager = context.getSystemService(AudioManager::class.java)

    // Android 6+ 推荐方式:AudioDeviceCallback
    private val deviceCallback = object : AudioManager.AudioDeviceCallback() {
        override fun onAudioDevicesAdded(addedDevices: Array<AudioDeviceInfo>) {
            for (device in addedDevices) {
                when (device.type) {
                    AudioDeviceInfo.TYPE_WIRED_HEADSET,
                    AudioDeviceInfo.TYPE_WIRED_HEADPHONES -> {
                        Log.d("Audio", "有线耳机已连接: ${device.productName}")
                        // 此时路由已自动切换,无需手动操作
                    }
                    AudioDeviceInfo.TYPE_BLUETOOTH_A2DP -> {
                        Log.d("Audio", "蓝牙耳机已连接: ${device.productName}")
                    }
                }
            }
        }

        override fun onAudioDevicesRemoved(removedDevices: Array<AudioDeviceInfo>) {
            for (device in removedDevices) {
                Log.d("Audio", "设备已断开: ${device.productName} (type=${device.type})")
            }
        }
    }

    fun startListening() {
        audioManager.registerAudioDeviceCallback(deviceCallback, Handler(Looper.getMainLooper()))
    }

    fun stopListening() {
        audioManager.unregisterAudioDeviceCallback(deviceCallback)
    }
}

10.2 案例二:指定音频输出设备

Android 6.0 引入了 setPreferredDevice() API,允许 App 指定输出到特定设备:

kotlin 复制代码
class PreferredDevicePlayer(private val context: Context) {

    private val audioManager = context.getSystemService(AudioManager::class.java)
    private var audioTrack: AudioTrack? = null

    fun playOnSpeaker(audioData: ByteArray, sampleRate: Int) {
        // 找到扬声器设备
        val speakerDevice = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
            .firstOrNull { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
            ?: run {
                Log.e("Audio", "扬声器不可用")
                return
            }

        val minBufferSize = AudioTrack.getMinBufferSize(
            sampleRate,
            AudioFormat.CHANNEL_OUT_STEREO,
            AudioFormat.ENCODING_PCM_16BIT
        )

        audioTrack = AudioTrack.Builder()
            .setAudioAttributes(AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .build())
            .setAudioFormat(AudioFormat.Builder()
                .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
                .setSampleRate(sampleRate)
                .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
                .build())
            .setBufferSizeInBytes(minBufferSize * 4)
            .setTransferMode(AudioTrack.MODE_STREAM)
            .build()

        // 强制指定输出设备:告知 APS 走这个设备
        // 注意:这会绕过设备优先级策略
        audioTrack?.preferredDevice = speakerDevice

        audioTrack?.play()
        audioTrack?.write(audioData, 0, audioData.size)
    }

    fun clearPreferredDevice() {
        // 清除后恢复 APS 的自动路由策略
        audioTrack?.preferredDevice = null
    }

    fun release() {
        audioTrack?.stop()
        audioTrack?.release()
        audioTrack = null
    }
}

注意 :setPreferredDevice() 是 App 层的设备偏好,优先级最高,会覆盖 APS 的自动路由决策。慎用------用户可能并不希望你的 App 劫持音频设备。

10.3 案例三:检测当前音频路由

kotlin 复制代码
fun getCurrentOutputDevice(audioManager: AudioManager): AudioDeviceInfo? {
    // 方法一:通过 AudioTrack 查询(Android 10+)
    // 创建一个临时 AudioTrack 来查询活跃设备
    val format = AudioFormat.Builder()
        .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
        .setSampleRate(44100)
        .setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
        .build()

    val track = AudioTrack.Builder()
        .setAudioAttributes(AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .build())
        .setAudioFormat(format)
        .setBufferSizeInBytes(4096)
        .build()

    // routedDevice 反映 APS 的路由决策结果
    val routedDevice = track.routedDevice
    track.release()
    return routedDevice
}

// 更简单的方式:监听路由变更
fun listenForRouteChanges(track: AudioTrack) {
    track.addOnRoutingChangedListener({ audioTrack ->
        val newDevice = audioTrack.routedDevice
        Log.d("Audio", "路由变更 → ${newDevice?.productName} (type=${newDevice?.type})")
    }, Handler(Looper.getMainLooper()))
}

十一、调试技巧

11.1 dumpsys 查看策略状态

dumpsys media.audio_policy 是 APS 调试的瑞士军刀,输出内容极其丰富:

bash 复制代码
# 查看完整策略状态
adb shell dumpsys media.audio_policy

# 关键输出节选:
#
# AudioPolicyManager state:
#  Audio Hw Modules:
#   - primary (2.0): 5 Outputs, 2 Inputs
#     Output: 0x8 Primary output flags: 0x10000(deep buffer)
#       Devices: AUDIO_DEVICE_OUT_SPEAKER
#       ...
#  Available input devices:
#   - type:0x80000004 name:builtin_mic id:1 handle:0
#
#  Phone state: NORMAL
#  Force use for COMMUNICATION: NONE
#  Force use for MEDIA: NONE
#
#  Outputs:
#   - 8 [MixerThread] DEVICE: AUDIO_DEVICE_OUT_SPEAKER
#       Profiles: PCM 16 Bits, 48000 Hz, stereo

重点关注:

  • Phone state:当前音频模式
  • Force use for XXX:ForceUse 状态
  • Outputs/Inputs:活跃的输入输出流

11.2 追踪路由决策日志

bash 复制代码
# 开启 APM 详细日志
adb shell setprop log.tag.APM_AudioPolicyManager V
adb shell setprop log.tag.AudioPolicyService V

# 过滤关键日志
adb logcat -s APM_AudioPolicyManager:V AudioPolicyService:V \
    | grep -E "getOutputForAttr|setDeviceConnection|setPhoneState"

典型的路由决策日志:

makefile 复制代码
APM_AudioPolicyManager: getOutputForAttr() usage 1, format 1, channelMask 3
APM_AudioPolicyManager: getOutputForDevice() strategy 0 device 0x2 flags 0x0
APM_AudioPolicyManager: Found output 8 for device AUDIO_DEVICE_OUT_WIRED_HEADSET

11.3 验证设备优先级

bash 复制代码
# 查看当前可用的输出设备
adb shell dumpsys media.audio_policy | grep "Available output"

# 手动触发设备连接(测试用)
adb shell media volume --set 10 --stream 3

# 查看音频焦点状态
adb shell dumpsys audio | grep -A5 "Audio Focus"

11.4 常见路由问题诊断

问题 1:插入耳机后声音仍从扬声器出来

bash 复制代码
# 检查内核是否检测到耳机插入
adb shell cat /sys/class/switch/h2w/state
# 0 = 未插入, 1 = 有耳机, 2 = 有耳机+麦克风

# 检查 APM 是否收到连接通知
adb logcat | grep "setDeviceConnectionState.*HEADSET.*AVAILABLE"

问题 2:通话音质差(回声、噪声)

bash 复制代码
# 检查是否正确进入 IN_CALL 模式
adb shell dumpsys media.audio_policy | grep "Phone state"
# 预期: Phone state: IN_CALL

# 检查 EC/NS 是否启用
adb shell dumpsys media.audio_policy | grep "Effect"

问题 3:蓝牙音频延迟高

bash 复制代码
# 检查蓝牙编解码格式
adb shell dumpsys bluetooth_manager | grep "A2dp"
# 查看是否使用了低延迟编解码(aptX LL, LDAC low latency等)

十二、总结

本篇深入解析了 AudioPolicyService 的工作原理:

核心概念 说明
职责分离 APS 做策略决策,AF 做音频处理,两者通过 IoHandle 协作
配置驱动 audio_policy_configuration.xml 定义拓扑,engine_configuration.xml 定义策略规则
优先级链 应用指定 > 音频模式 > ForceUse > 设备优先级列表
自动 Failover 设备断开时自动切换到下一优先级设备
Android 15 升级 AIDL 替代 HIDL,空间音频原生支持,更严格权限控制

理解了 AudioPolicyService,你就理解了 Android 音频路由的"大脑"。下一篇我们将深入音频焦点管理------当多个 App 同时要播放音频时,谁有权发出声音?Android 的焦点仲裁机制又是如何工作的?敬请期待。

踩坑提示 :setPreferredDevice() 是一把双刃剑。用它可以精确控制音频路由,但一旦指定了设备,APS 的自动路由逻辑就不再起作用。笔者曾见过一个 Bug:App 指定了扬声器输出,用户接电话时却发现所有声音都跑到扬声器里------因为开发者忘记在 onAudioFocusChange() 中清除 preferredDevice。记住:用完要清理,audioTrack.preferredDevice = null。

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui