1. 摘要
Android Audio Policy 子系统通过 HwModule、IOProfile、AudioRoute、DeviceDescriptor、AudioProfile 五类核心数据结构,完成音频能力的描述、设备的路由匹配与运行时的动态管理。
本文以 USB 音频设备为线索,首先梳理 XML 配置文件到 C++ 对象的映射关系与类职责分工,然后深入两条关键运行时流程:
- 设备插入时的能力发现:从
HAL层动态创建DeviceDescriptor,到向IOProfile按需添加AudioProfile的完整过程; - 录音请求的路由匹配:从应用音频属性出发,经过
IOProfile兼容性判定,最终选定可用输入流的匹配算法。
最后,基于前述机制分析一个真实问题——USB 摄像头与 USB 耳麦共存时的录音失败场景,给出根因与配置级解决方案。
2. 配置文件与类的映射
先来看个配置文件usb_audio_policy_configuration.xml:
<module name="usb" halVersion="2.0" >
<mixPorts>
<mixPort name="usb_accessory output" role="source">
<profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
samplingRates="44100" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
</mixPort>
<mixPort name="usb_headset input" role="sink"/>
</mixPorts>
<devicePorts>
<devicePort tagName="USB Host Out" type="AUDIO_DEVICE_OUT_USB_ACCESSORY" role="sink">
<profile name="" format="AUDIO_FORMAT_PCM_16_BIT"
samplingRates="44100" channelMasks="AUDIO_CHANNEL_OUT_STEREO"/>
</devicePort>
<devicePort tagName="USB Headset In" type="AUDIO_DEVICE_IN_USB_HEADSET" role="source"/>
</devicePorts>
<routes>
<route type="mix" sink="usb_headset input"
sources="USB Headset In"/>
</routes>
</module>
配置文件经过解析后,XML 元素与C++ 类的对应关系如下:
XML 元素 → C++ 类
─────────────────────────────────────────────
<module> → HwModule
<mixPort> → IOProfile (role=source → 播放, role=sink → 录音)
<profile> → IOProfile 内的 AudioProfile 列表
format / samplingRates / → AudioProfile 的字段
channelMasks
<devicePort> → DeviceDescriptor (也是 IOProfile 的子类)
<profile> → DeviceDescriptor 内的 AudioProfile 列表
<route> → AudioRoute (连接 mixPort 和 devicePort)
3. 类关系概览
在 Android Audio Policy 子系统中,这几个类的职责分工如下:
HwModule
硬件音频模块(如 primary、a2dp、usb)的抽象,持有该模块下所有输入/输出 IOProfile。对应 XML 中的 <module>标签。
IOProfile
继承自 AudioPort 和 PolicyAudioPort,代表一条输入或输出流的能力描述(采样率、格式、声道数、flag 等),对应 XML 中的 <mixPort>标签。
HwModule::mInputProfiles 和 HwModule::mOutputProfiles 都是 IOProfile 的集合,IOProfile也作为 HwModule::mPorts 中的元素被统一管理。
IOProfile.mSupportedDevices 的设备来源
IOProfile.mSupportedDevices包含DeviceDescriptor对象的指针:
静态来源(解析XML里的route时):
在HwModule::refreshSupportedDevices()中:
- 把
route的sources对应的DeviceDescriptor添加到HwModule.mInputProfiles.IOProfile.mSupportedDevices - 把
route的sink对应的DeviceDescriptor添加到HwModule.mOutputProfiles.IOProfile.mSupportedDevices
动态来源(设备插入时):
在HwModuleCollection::createDevice()或AudioPolicyManagerCustomWhaleImpl::getUsbDeviceDescriptor()中:
- 创建一个新的
DeviceDescriptor对象 - 调用
attach()把它绑定到usb HwModule IOProfile::addSupportedDevice()把它添加到mSupportedDevices
DeviceDescriptor
继承自 DeviceDescriptorBase 和 PolicyAudioPort,代表物理音频设备(如 USB Headset In、Speaker、Built-In Mic)的抽象。
- 静态设备:定义来源于 XML 中的
<devicePort>标签,在系统启动时解析配置文件即创建,作为"能力声明"始终存在。并作为 HwModule::mPorts 和 HwModule::mDeclaredDevices 中的元素被统一管理; - 动态设备:真正的物理设备插入时,会在
AudioPolicyManager::setDeviceConnectionState()流程中基于静态模板创建带具体地址的动态设备实例,通过attach()绑定到所属的HwModule,并被加入到HwModule::mDynamicDevices中统一管理。也被IOProfile::mSupportedDevices引用,表示该IOProfile支持此设备。
AudioRoute
定义了 IOProfile(mixPort)与 DeviceDescriptor(devicePort)之间的连接关系,对应 XML 中的 <route>标签。
方向规则:连接方向与音频数据流向相反,遵循"sink 消费源,source 产生数据"的原则。
| 场景 | 数据源 | 数据目的 | AudioRoute 表现 |
|---|---|---|---|
| 输出 (Playback) | IOProfile (role=source) | DeviceDescriptor (role=sink) | mSources → IOProfile, mSink → DeviceDescriptor |
| 输入 (Recording) | DeviceDescriptor (role=source) | IOProfile (role=sink) | mSources → DeviceDescriptor, mSink → IOProfile |
其连接方向与音频数据流向相反,遵循“sink 消费源,source 产生数据”的原则:
输出音频场景(Playback):数据从应用输出至物理设备。此时 IOProfile(mixPort,role="source")作为数据源,DeviceDescriptor(devicePort,role="sink")作为数据目的。在 AudioRoute 中表现为:mSources 指向 IOProfile,mSink 指向 DeviceDescriptor。
<!-- 输出示例 -->
<route type="mix" sink="USB Device Out" sources="usb_device output"/>
<!-- 输入示例 -->
<route type="mix" sink="usb_device input" sources="USB Device In"/>
每个 HwModule::mRoutes 持有该模块下所有路由规则。在 refreshSupportedDevices() 中,系统遍历每条 AudioRoute,根据方向将 mSources 或 mSink 中的设备添加到对应 IOProfile::mSupportedDevices 中,从而建立IO能力与物理设备的绑定关系。
AudioPort(基类)
是所有端口(IOProfile 与 DeviceDescriptor)的基类,内部持有 mProfiles(类型 AudioProfileVector),描述该端口支持的音频能力。
AudioProfileVector
是 std::vector<sp<AudioProfile>> 的封装,提供增删查和能力协商方法。
AudioProfile
是一条具体的能力记录:一种编码格式 + 一组采样率 + 一组声道掩码。对应 XML 中的 <profile> 标签。当mixPort或devicePort标签下没有声明profile时,在解析XML过程中,会自动生成一个全 dynamic 用于占位的 AudioProfile,三个维度(格式/声道/采样率)都标记为dynamic,表示不约束任何值。并将这个全 dynamic 的占位AudioProfile添加到IOProfile和DeviceDescriptor。
4. 音频设备插入时的能力动态发现
现在以USB 耳机 USB Headset In设备接入为例,探索DeviceDescriptor 与 AudioProfile 的动态创建流程 。
4.1 DeviceDescriptor 的动态创建与绑定
UsbHostManager#usbDeviceAdded()
↓
UsbAlsaManager#usbDeviceAdded()
↓
UsbAlsaManager#selectAlsaDevice()
↓
UsbAlsaDevice#start()
↓
UsbAlsaDevice#startInput()
↓
UsbAlsaDevice#startDevice()
↓
UsbAlsaDevice#updateWiredDeviceConnectionState()
↓
AudioService#setWiredDeviceConnectionState()
↓
AudioDeviceBroker#setWiredDeviceConnectionState()
↓
AudioDeviceInventory#setWiredDeviceConnectionState()
↓
AudioDeviceBroker#postSetWiredDeviceConnectionState()
↓
sendLMsg(MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE)
↓
AudioDeviceInventory#onSetWiredDeviceConnectionState
↓
AudioDeviceInventory:handleDeviceConnection()
↓
AudioSystem:setDeviceConnectionState()
↓
android_media_AudioSystem_setDeviceConnectionState()
↓
AudioSystem:setDeviceConnectionState()
↓
AudioPolicyService::setDeviceConnectionState()
↓
AudioPolicyManager::setDeviceConnectionState()
↓
AudioPolicyManagerCustomImplwhale::setDeviceConnectionStateInt()
↓
AudioPolicyManagerCustomWhaleImpl::getUsbDeviceDescriptor()
AudioPolicyManagerCustomWhaleImpl::getUsbDeviceDescriptor()的源码如下:
sp<DeviceDescriptor> AudioPolicyManagerCustomWhaleImpl::getUsbDeviceDescriptor(const audio_devices_t deviceType,
const char *address,
const char *name,
const audio_format_t encodedFormat,
bool allowToCreate,
bool matchAddress)
{
//步骤1: 判断要用哪个HwModule
String8 devAddress = (address == nullptr || !matchAddress) ? String8("") : String8(address);
bool usbDeviceInPrimary = allowToCreate ? isUsbDeviceSupportOffload(deviceType, address, name)
:(mUsbOffloadDevices.valueFor(toString(deviceType) + std::string(address)) != 0);
sp<HwModule> hwUsbModule = usbDeviceInPrimary ? mApm->mHwModules.getModuleFromName(AUDIO_HARDWARE_MODULE_ID_PRIMARY)
: mApm->mHwModules.getModuleFromName(AUDIO_HARDWARE_MODULE_ID_USB);
......
//步骤2: 创建DeviceDescriptor并和HwModule绑定
sp<DeviceDescriptor> device = new DeviceDescriptor(deviceType, name, address);
device->setName(name);
device->setEncodedFormat(encodedFormat);
device->setDynamic();
// Add the device to the list of dynamic devices
hwUsbModule->addDynamicDevice(device);
// Reciprocally attach the device to the module
device->attach(hwUsbModule);
ALOGD("%s: adding dynamic device %s to module %s", __FUNCTION__,
device->toString().c_str(), hwUsbModule->getName());
//步骤3: 将DeviceDescriptor加入到适配的IOProfile里
const auto &profiles = (audio_is_output_device(deviceType) ? hwUsbModule->getOutputProfiles() :
hwUsbModule->getInputProfiles());
for (const auto &profile : profiles) {
// Add the device as supported to all profile supporting "weakly" or not the device
// according to its type
if (profile->supportsDevice(device, false /*matchAddress*/)) {
// @todo quid of audio profile? import the profile from device of the same type?
const auto &isoTypeDeviceForProfile =
profile->getSupportedDevices().getDevice(deviceType, String8(), AUDIO_FORMAT_DEFAULT);
device->importAudioPortAndPickAudioProfile(isoTypeDeviceForProfile, true /* force */);
ALOGV("%s: adding device %s to profile %s", __FUNCTION__,
device->toString().c_str(), profile->getTagName().c_str());
profile->addSupportedDevice(device);
}
}
return device;
}
AudioPolicyManagerCustomWhaleImpl::getUsbDeviceDescriptor()做了这3件事:
-
判断是要将新的
DeviceDescriptorattach到哪个HwModule: 展锐平台的USB offload模式下,USB音频数据由primary HAL的DSP直接处理,不经过USB HAL,所以必须归primary HwModule。而普通USB录音走USB HAL,归usb HwModule。这里不支持USB offload模式所以走USB HAL,归usb HwModule。 -
创建代表
USB Headset In的DeviceDescriptor,加入到usb HwModule里的mDynamicDevices, 并attach到usb HwModule。 -
遍历
usb HwModule里的IOProfile, 从IOProfile.mSupportedDevices中查找是否有跟USB Headset In一样type="AUDIO_DEVICE_IN_USB_HEADSET"的DeviceDescriptor,有的话表示该IOProfile支持"AUDIO_DEVICE_IN_USB_HEADSET"类型的设备,USB Headset In的DeviceDescriptor可以加入到IOProfile.mSupportedDevices。
因此,在XML里配置一个静态的type="AUDIO_DEVICE_IN_USB_HEADSET"的devicePort和对应的route就可以让系统支持"AUDIO_DEVICE_IN_USB_HEADSET"类型的设备。如:
<mixPorts>
<mixPort name="usb_headset input" role="sink"/>
</mixPorts>
<devicePorts>
<devicePort tagName="USB Headset In" type="AUDIO_DEVICE_IN_USB_HEADSET" role="source"/>
</devicePorts>
<routes>
<route type="mix" sink="usb_headset input"
sources="USB Headset In"/>
</routes>
此时执行dumpsys media.audio_policy命令可以看到:
Hardware modules (5):
4. Handle: 34; "usb"
- Input MixPorts (2):
2. "usb_headset input"; 0x0000 (AUDIO_INPUT_FLAG_NONE)
- Profiles (3):
1. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_DEFAULT (0x0)
AUDIO_ENCAPSULATION_TYPE_NONE
2. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_PCM_16_BIT (0x1)
AUDIO_ENCAPSULATION_TYPE_NONE
3. ""; [dynamic format]; AUDIO_FORMAT_PCM_16_BIT (0x1)
sampling rates: 48000
channel masks: 0x000c, 0x80000003
AUDIO_ENCAPSULATION_TYPE_NONE
- Supported devices (2):
1. Port ID: 1; "USB-Audio - USB Audio Device"; {AUDIO_DEVICE_IN_USB_HEADSET, @:card=1;device=0;}
Encapsulation modes: 0, metadata types: 0
"USB-Audio - USB Audio Device"
2. "USB Headset In"; {AUDIO_DEVICE_IN_USB_HEADSET, @:}
Encapsulation modes: 0, metadata types: 0
- maxOpenCount: 1; curOpenCount: 0
- maxActiveCount: 1; curActiveCount: 0
- recommendedMuteDurationMs: 0 ms
其中:
1. Port ID: 1; "USB-Audio - USB Audio Device"
address: @:card=1;device=0; ← 运行时动态创建
2. "USB Headset In"
address: @: ← XML 静态定义
静态设备 — 来自 XML 配置:
<devicePort tagName="USB Headset In" type="AUDIO_DEVICE_IN_USB_HEADSET" role="source"/>
<route type="mix" sink="usb_headset input" sources="USB Headset In"/>
解析时通过 <route>加入 mSupportedDevices。地址为空 @:,因为还没有真实设备。
动态设备 — USB 设备插入时,Audio HAL 扫描到 card=2, device=0,动态创建一个同类型但带具体地址的 DeviceDescriptor,并关联到该IOProfile。
| 静态 "USB Headset In" | 动态 "USB-Audio - USB Audio Device" | |
|---|---|---|
| 存在时机 | 始终存在 | 仅设备插入时 |
| address | @:(空) | @:card=2;device=0;(具体) |
| 用途 | 占位模板,保证 profile 始终有支持设备 | 实际录音时使用的真实设备 |
| Port ID | 无(未 attach) | 2(已 attach 到 HAL) |
静态的那个是"占位符",确保在没插 USB 设备时 IOProfile 结构完整;动态的那个才是真正打开录音流时用的设备。选设备时优先选有Port ID的(已连接的),静态的仅作 fallback。
4.2 AudioProfile 的动态发现与添加
在AudioPolicyManagerCustomImplwhale::setDeviceConnectionStateInt()里创建完DeviceDescriptor后,代码来到AudioPolicyManager::updateAudioProfiles():
AudioPolicyManager::setDeviceConnectionState()
↓
AudioPolicyManagerCustomImplwhale::setDeviceConnectionStateInt()
↓
AudioPolicyManager::setDeviceConnectionStateInt()
↓
AudioPolicyManager::checkInputsForDevice()
↓
AudioPolicyManager::updateAudioProfiles()
frameworks/av/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
void AudioPolicyManager::updateAudioProfiles(const sp<DeviceDescriptor>& devDesc,
audio_io_handle_t ioHandle,
AudioProfileVector &profiles)
{
String8 reply;
audio_devices_t device = devDesc->type();
// Format MUST be checked first to update the list of AudioProfile
if (profiles.hasDynamicFormat()) {
reply = mpClientInterface->getParameters(
ioHandle, String8(AudioParameter::keyStreamSupportedFormats));
ALOGV("%s: supported formats %d, %s", __FUNCTION__, ioHandle, reply.string());
AudioParameter repliedParameters(reply);
if (repliedParameters.get(
String8(AudioParameter::keyStreamSupportedFormats), reply) != NO_ERROR) {
ALOGE("%s: failed to retrieve format, bailing out", __FUNCTION__);
return;
}
FormatVector formats = formatsFromString(reply.string());
mReportedFormatsMap[devDesc] = formats;
if (device == AUDIO_DEVICE_OUT_HDMI
|| isDeviceOfModule(devDesc, AUDIO_HARDWARE_MODULE_ID_MSD)) {
modifySurroundFormats(devDesc, &formats);
}
addProfilesForFormats(profiles, formats);
}
for (audio_format_t format : profiles.getSupportedFormats()) {
ChannelMaskSet channelMasks;
SampleRateSet samplingRates;
AudioParameter requestedParameters;
requestedParameters.addInt(String8(AudioParameter::keyFormat), format);
if (profiles.hasDynamicRateFor(format)) {
reply = mpClientInterface->getParameters(
ioHandle,
requestedParameters.toString() + ";" +
AudioParameter::keyStreamSupportedSamplingRates);
ALOGV("%s: supported sampling rates %s", __FUNCTION__, reply.string());
AudioParameter repliedParameters(reply);
if (repliedParameters.get(
String8(AudioParameter::keyStreamSupportedSamplingRates), reply) == NO_ERROR) {
samplingRates = samplingRatesFromString(reply.string());
}
}
if (profiles.hasDynamicChannelsFor(format)) {
reply = mpClientInterface->getParameters(ioHandle,
requestedParameters.toString() + ";" +
AudioParameter::keyStreamSupportedChannels);
ALOGV("%s: supported channel masks %s", __FUNCTION__, reply.string());
AudioParameter repliedParameters(reply);
if (repliedParameters.get(
String8(AudioParameter::keyStreamSupportedChannels), reply) == NO_ERROR) {
channelMasks = channelMasksFromString(reply.string());
if (device == AUDIO_DEVICE_OUT_HDMI
|| isDeviceOfModule(devDesc, AUDIO_HARDWARE_MODULE_ID_MSD)) {
modifySurroundChannelMasks(&channelMasks);
}
}
}
addDynamicAudioProfileAndSort(
profiles, new AudioProfile(format, channelMasks, samplingRates));
}
}
相关log:
APM_AudioPolicyManager: checkInputsForDevice(): adding profile 0 from module usb
modules.usbaudio.audio_hal: in_set_parameters() keys:card=1;device=0
modules.usbaudio.audio_hal: device_get_parameters = sup_formats=AUDIO_FORMAT_PCM_16_BIT
modules.usbaudio.audio_hal: device_get_parameters = sup_sampling_rates=48000
modules.usbaudio.audio_hal: device_get_parameters = sup_channels=AUDIO_CHANNEL_IN_STEREO|AUDIO_CHANNEL_INDEX_MASK_2
APM_AudioPolicyManager: checkInputsForDevice(): adding input 54
在接入USB Headset In设备之前, 名为"usb_headset input"的MixPort只有一个AudioProfile:
$ dumpsys media.audio_policy
2. "usb_headset input"; 0x0000 (AUDIO_INPUT_FLAG_NONE)
- Profiles (1):
1. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_DEFAULT (0x0)
AUDIO_ENCAPSULATION_TYPE_NONE
接入USB Headset In设备之后, 名为"usb_headset input"的MixPort就会多出2个AudioProfile:
$ dumpsys media.audio_policy
2. "usb_headset input"; 0x0000 (AUDIO_INPUT_FLAG_NONE)
- Profiles (3):
1. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_DEFAULT (0x0)
AUDIO_ENCAPSULATION_TYPE_NONE
2. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_PCM_16_BIT (0x1)
AUDIO_ENCAPSULATION_TYPE_NONE
3. ""; [dynamic format]; AUDIO_FORMAT_PCM_16_BIT (0x1)
sampling rates: 48000
channel masks: 0x000c, 0x80000003
AUDIO_ENCAPSULATION_TYPE_NONE
结合AudioPolicyManager::updateAudioProfiles()的逻辑和以上信息,后面的2个AudioProfile的添加流程如下:
初始状态
只有一个profile:
Profile 1: format=DEFAULT, dynamic format, dynamic channels, dynamic rates
Step 1: 查格式
if (profiles.hasDynamicFormat()) { // Profile 1 是 dynamic format → true
reply = mpClientInterface->getParameters(ioHandle, "streamSupportedFormats");
// HAL 返回: "AUDIO_FORMAT_PCM_16_BIT"
FormatVector formats = {PCM_16_BIT};
addProfilesForFormats(profiles, formats); // ← 关键
}
addProfilesForFormats() 为每个格式创建一个新profile(dynamic channels + dynamic rates),加入列表:
Profile 1: DEFAULT, dynamic format, dynamic channels, dynamic rates (原始)
Profile 2: PCM_16_BIT, dynamic channels, dynamic rates (新增) ← 这就是 dump 里的 Profile 2
Step 2: 遍历已支持的格式,查采样率和声道
for (audio_format_t format : profiles.getSupportedFormats()) {
// format = PCM_16_BIT (唯一非 dynamic 的格式)
查采样率:
if (profiles.hasDynamicRateFor(PCM_16_BIT)) { // Profile 2 是 dynamic rate → true
reply = mpClientInterface->getParameters(ioHandle, "format=PCM_16_BIT;streamSupportedSamplingRates");
// HAL 返回: "48000"
samplingRates = {48000};
}
查声道:
if (profiles.hasDynamicChannelsFor(PCM_16_BIT)) { // Profile 2 是 dynamic channels → true
reply = mpClientInterface->getParameters(ioHandle, "format=PCM_16_BIT;streamSupportedChannels");
// HAL 返回: "0x000c,0x80000003"
channelMasks = {0x000c, 0x80000003};
}
创建精确 profile:
addDynamicAudioProfileAndSort(profiles,
new AudioProfile(PCM_16_BIT, {0x000c, 0x80000003}, {48000}));
这创建了 Profile 3。
最终结果
Profile 1: DEFAULT, dynamic format, dynamic channels, dynamic rates (原始兜底)
Profile 2: PCM_16_BIT, dynamic channels, dynamic rates (格式已确定,其余待查)
Profile 3: PCM_16_BIT, 48000Hz, {0x000c, 0x80000003} (完全确定)
流程图
初始: [Profile 1: 全 dynamic]
↓ addProfilesForFormats(PCM_16_BIT)
中间: [Profile 1: 全 dynamic]
[Profile 2: PCM_16_BIT, dynamic rate, dynamic channels]
↓ addDynamicAudioProfileAndSort(PCM_16_BIT, 48000, {0x000c, 0x80000003})
最终: [Profile 1: 全 dynamic] ← 兜底
[Profile 2: PCM_16_BIT, dynamic] ← 格式固定,其余通配
[Profile 3: PCM_16_BIT, 48000, {0x000c, 0x80000003}] ← 精确值
三个 profile 形成从宽到窄的梯度:全通配 → 格式固定 → 完全确定,兼容匹配时从前往后找,精确匹配时从后往前找。
5. App 录音请求时的路由匹配路径
相关log:
I APM_AudioPolicyManager: getInputForAttr() source 7, sampling rate 48000, format 0x1, channel mask 0x10, session 177, flags 0 attributes={ Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_VOICE_COMMUNICATION Flags: 0x2800 Tags: } requested device ID 0
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x1
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x1, 0x2
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x1, 0x2, 0x10000
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x1, 0x2, 0x10000, 0x4000000
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x80000004
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x80000004, 0x80000040
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x80000004, 0x80000040, 0x80000080
V APM::Devices: DeviceVector::refreshTypes() mDeviceTypes 0x80000004, 0x80000040, 0x80000080, 0x80002000
D APM::AudioPolicyEngine/InputSource: get: 0x82000000 for inputSource AUDIO_SOURCE_VOICE_COMMUNICATION
D APM::AudioPolicyEngine: getInputDeviceForAttributes deviceType = 0x82000000, address =
D APM::Devices: DeviceVector::getDevice() for type 82000000 address "" found 0xb4000077d3d59de0 format 00000000
D APM_AudioPolicyManager: getInputForAttr found device type is 0x82000000
D APM_AudioPolicyManager: getInputForDevice attributes.source = 7
D APM_AudioPolicyManager: getInputProfile, flags = 00000020, device = AUDIO_DEVICE_IN_USB_HEADSET, @:card=1;device=0;
D APM_AudioPolicyManager: getInputProfile: hwModule: primary channelMask = 00000010
I AudioPolicyManagerCustomImplwhale: skip <primary input> profile or output whose module is different from <AUDIO_DEVICE_IN_USB_HEADSET, @:card=1;device=0;>
当App发出录音请求时,代码会走AudioPolicyManager里的:
getInputForAttr(source, flags)
│
├─ Engine::getInputDeviceForAttributes(source)
│ → 查 PFW 配置,返回设备 (如 USB_HEADSET)
│
└─ getInputForDevice(device, ..., attributes, flags)
│
└─ getInputProfile(device, samplingRate, format, channelMask, flags)
→ 遍历模块找 isCompatibleProfile 的 IOProfile
→ 返回 profile
│
→ 打开输入流,返回 input handle
| 函数 | 作用 |
|---|---|
| getInputForAttr() | 根据音频属性(source, flags)选输入流,返回 input handle |
| Engine::getInputDeviceForAttributes() | 策略决策。根据 source 查 parameter-framework,决定用哪个输入设备 |
| getInputForDevice() | 设备→流。给定设备,找能路由到该设备的 IOProfile 并打开输入流 |
| getInputProfile() | 设备→profile。给定设备+参数,遍历所有模块找兼容的 IOProfile |
至于Engine::getInputDeviceForAttributes()怎么使用 parameter-framework选择输入设备的,后续再研究。
bool IOProfile::isCompatibleProfile(const DeviceVector &devices,
uint32_t samplingRate,
uint32_t *updatedSamplingRate,
audio_format_t format,
audio_format_t *updatedFormat,
audio_channel_mask_t channelMask,
audio_channel_mask_t *updatedChannelMask,
// FIXME type punning here
uint32_t flags,
bool exactMatchRequiredForInputFlags) const
{
const bool isPlaybackThread =
getType() == AUDIO_PORT_TYPE_MIX && getRole() == AUDIO_PORT_ROLE_SOURCE;
const bool isRecordThread =
getType() == AUDIO_PORT_TYPE_MIX && getRole() == AUDIO_PORT_ROLE_SINK;
ALOG_ASSERT(isPlaybackThread != isRecordThread);
if (!devices.isEmpty()) {
if (!mSupportedDevices.containsAllDevices(devices)) {
return false;
}
}
if (!audio_is_valid_format(format) ||
(isPlaybackThread && (samplingRate == 0 || !audio_is_output_channel(channelMask))) ||
(isRecordThread && (!audio_is_input_channel(channelMask)))) {
return false;
}
audio_format_t myUpdatedFormat = format;
audio_channel_mask_t myUpdatedChannelMask = channelMask;
uint32_t myUpdatedSamplingRate = samplingRate;
const struct audio_port_config config = {
.config_mask = AUDIO_PORT_CONFIG_ALL & ~AUDIO_PORT_CONFIG_GAIN,
.sample_rate = samplingRate,
.channel_mask = channelMask,
.format = format,
};
if (isRecordThread)
{
if ((flags & AUDIO_INPUT_FLAG_MMAP_NOIRQ) != 0) {
if (checkExactAudioProfile(&config) != NO_ERROR) {
return false;
}
} else if (checkCompatibleAudioProfile(
myUpdatedSamplingRate, myUpdatedChannelMask, myUpdatedFormat) != NO_ERROR) {
return false;
}
} else {
if (checkExactAudioProfile(&config) != NO_ERROR) {
return false;
}
}
const uint32_t mustMatchOutputFlags =
AUDIO_OUTPUT_FLAG_DIRECT|AUDIO_OUTPUT_FLAG_HW_AV_SYNC|AUDIO_OUTPUT_FLAG_MMAP_NOIRQ;
if (isPlaybackThread && (((getFlags() ^ flags) & mustMatchOutputFlags)
|| (getFlags() & flags) != flags)) {
return false;
}
// The only input flag that is allowed to be different is the fast flag.
// An existing fast stream is compatible with a normal track request.
// An existing normal stream is compatible with a fast track request,
// but the fast request will be denied by AudioFlinger and converted to normal track.
if (isRecordThread && ((getFlags() ^ flags) &
~(exactMatchRequiredForInputFlags ? AUDIO_INPUT_FLAG_NONE : AUDIO_INPUT_FLAG_FAST))) {
return false;
}
if (updatedSamplingRate != NULL) {
*updatedSamplingRate = myUpdatedSamplingRate;
}
if (updatedFormat != NULL) {
*updatedFormat = myUpdatedFormat;
}
if (updatedChannelMask != NULL) {
*updatedChannelMask = myUpdatedChannelMask;
}
return true;
}
frameworks/av/services/audiopolicy/common/managerdefinitions/include/PolicyAudioPort.h
status_t checkCompatibleAudioProfile(uint32_t &samplingRate,
audio_channel_mask_t &channelMask,
audio_format_t &format) const
{
return checkCompatibleProfile(
asAudioPort()->getAudioProfiles(), samplingRate, channelMask, format,
asAudioPort()->getType(), asAudioPort()->getRole());
}
将IOProfile转成AudioPort,然后取其mProfiles,也就是IOProfile所管理的AudioProfile列表进入checkCompatibleProfile()做匹配。
frameworks/av/services/audiopolicy/common/managerdefinitions/src/AudioProfileVectorHelper.cpp
status_t checkCompatibleProfile(const AudioProfileVector &audioProfileVector,
uint32_t &samplingRate,
audio_channel_mask_t &channelMask,
audio_format_t &format,
audio_port_type_t portType,
audio_port_role_t portRole)
{
if (audioProfileVector.empty()) {
return NO_ERROR;
}
const bool checkInexact = // when port is input and format is linear pcm
portType == AUDIO_PORT_TYPE_MIX && portRole == AUDIO_PORT_ROLE_SINK
&& audio_is_linear_pcm(format);
// iterate from best format to worst format (reverse order)
for (ssize_t i = audioProfileVector.size() - 1; i >= 0 ; --i) {
const sp<AudioProfile> profile = audioProfileVector.at(i);
audio_format_t formatToCompare = profile->getFormat();
if (formatToCompare == format ||
(checkInexact
&& formatToCompare != AUDIO_FORMAT_DEFAULT
&& audio_is_linear_pcm(formatToCompare))) {
// Compatible profile has been found, checks if this profile has compatible
// rate and channels as well
audio_channel_mask_t updatedChannels;
uint32_t updatedRate;
if (checkCompatibleChannelMask(profile, channelMask, updatedChannels,
portType, portRole) == NO_ERROR &&
checkCompatibleSamplingRate(profile, samplingRate, updatedRate) == NO_ERROR) {
// for inexact checks we take the first linear pcm format due to sorting.
format = formatToCompare;
channelMask = updatedChannels;
samplingRate = updatedRate;
return NO_ERROR;
}
}
}
return BAD_VALUE;
}
status_t checkCompatibleChannelMask(const sp<AudioProfile> &audioProfile,
audio_channel_mask_t channelMask,
audio_channel_mask_t &updatedChannelMask,
audio_port_type_t portType,
audio_port_role_t portRole)
{
const ChannelMaskSet channelMasks = audioProfile->getChannels();
if (channelMasks.empty()) {
updatedChannelMask = channelMask;
return NO_ERROR;
}
const bool isRecordThread = portType == AUDIO_PORT_TYPE_MIX && portRole == AUDIO_PORT_ROLE_SINK;
const bool isIndex = audio_channel_mask_get_representation(channelMask)
== AUDIO_CHANNEL_REPRESENTATION_INDEX;
const uint32_t channelCount = audio_channel_count_from_in_mask(channelMask);
int bestMatch = 0;
for (const auto &supported : channelMasks) {
if (supported == channelMask) {
// Exact matches always taken.
updatedChannelMask = channelMask;
return NO_ERROR;
}
// AUDIO_CHANNEL_NONE (value: 0) is used for dynamic channel support
if (isRecordThread && supported != AUDIO_CHANNEL_NONE) {
// Approximate (best) match:
// The match score measures how well the supported channel mask matches the
// desired mask, where increasing-is-better.
//
// TODO: Some tweaks may be needed.
// Should be a static function of the data processing library.
//
// In priority:
// match score = 1000 if legacy channel conversion equivalent (always prefer this)
// OR
// match score += 100 if the channel mask representations match
// match score += number of channels matched.
// match score += 100 if the channel mask representations DO NOT match
// but the profile has positional channel mask and less than 2 channels.
// This is for audio HAL convention to not list index masks for less than 2 channels
//
// If there are no matched channels, the mask may still be accepted
// but the playback or record will be silent.
const bool isSupportedIndex = (audio_channel_mask_get_representation(supported)
== AUDIO_CHANNEL_REPRESENTATION_INDEX);
const uint32_t supportedChannelCount = audio_channel_count_from_in_mask(supported);
int match;
if (isIndex && isSupportedIndex) {
// index equivalence
match = 100 + __builtin_popcount(
audio_channel_mask_get_bits(channelMask)
& audio_channel_mask_get_bits(supported));
} else if (isIndex && !isSupportedIndex) {
const uint32_t equivalentBits = (1 << supportedChannelCount) - 1 ;
match = __builtin_popcount(
audio_channel_mask_get_bits(channelMask) & equivalentBits);
if (supportedChannelCount <= FCC_2) {
match += 100;
}
} else if (!isIndex && isSupportedIndex) {
const uint32_t equivalentBits = (1 << channelCount) - 1;
match = __builtin_popcount(
equivalentBits & audio_channel_mask_get_bits(supported));
} else {
// positional equivalence
match = 100 + __builtin_popcount(
audio_channel_mask_get_bits(channelMask)
& audio_channel_mask_get_bits(supported));
switch (supported) {
case AUDIO_CHANNEL_IN_FRONT_BACK:
case AUDIO_CHANNEL_IN_STEREO:
if (channelMask == AUDIO_CHANNEL_IN_MONO) {
match = 1000;
}
break;
case AUDIO_CHANNEL_IN_MONO:
if (channelMask == AUDIO_CHANNEL_IN_FRONT_BACK
|| channelMask == AUDIO_CHANNEL_IN_STEREO) {
match = 1000;
}
break;
default:
break;
}
}
if (match > bestMatch) {
bestMatch = match;
updatedChannelMask = supported;
}
}
}
return bestMatch > 0 ? NO_ERROR : BAD_VALUE;
}
status_t checkCompatibleSamplingRate(const sp<AudioProfile> &audioProfile,
uint32_t samplingRate,
uint32_t &updatedSamplingRate)
{
ALOG_ASSERT(samplingRate > 0);
const SampleRateSet sampleRates = audioProfile->getSampleRates();
if (sampleRates.empty()) {
updatedSamplingRate = samplingRate;
return NO_ERROR;
}
// Search for the closest supported sampling rate that is above (preferred)
// or below (acceptable) the desired sampling rate, within a permitted ratio.
// The sampling rates are sorted in ascending order.
auto desiredRate = sampleRates.lower_bound(samplingRate);
// Prefer to down-sample from a higher sampling rate, as we get the desired frequency spectrum.
if (desiredRate != sampleRates.end()) {
if (*desiredRate / AUDIO_RESAMPLER_DOWN_RATIO_MAX <= samplingRate) {
updatedSamplingRate = *desiredRate;
return NO_ERROR;
}
}
// But if we have to up-sample from a lower sampling rate, that's OK.
if (desiredRate != sampleRates.begin()) {
uint32_t candidate = *(--desiredRate);
if (candidate * AUDIO_RESAMPLER_UP_RATIO_MAX >= samplingRate) {
updatedSamplingRate = candidate;
return NO_ERROR;
}
}
// leave updatedSamplingRate unmodified
return BAD_VALUE;
}
IOProfile::isCompatibleProfile()
PolicyAudioPort:
checkCompatibleProfile()
AudioProfileVectorHelper:
checkCompatibleProfile()
checkCompatibleChannelMask()
checkCompatibleSamplingRate()
以如下录音请求参数:
APM_AudioPolicyManager: getInputForAttr() source 7, sampling rate 48000, format 0x1, channel mask 0x10, session 177, flags 0 attributes={ Content type: AUDIO_CONTENT_TYPE_UNKNOWN Usage: AUDIO_USAGE_UNKNOWN Source: AUDIO_SOURCE_VOICE_COMMUNICATION Flags: 0x2800 Tags: } requested device ID 0
为例,IOProfile::isCompatibleProfile()匹配流程如下:
Step 1. 确定方向
getType() = MIX, getRole() = SINK
isPlaybackThread = false
isRecordThread = true ✓
Step 2. 设备兼容
请求设备: AUDIO_DEVICE_IN_USB_HEADSET
mSupportedDevices: {USB_HEADSET(card=1;dev=0), USB_HEADSET(空地址)}
→ containsAllDevices ✓
Step 3. 参数合法性
format = PCM_16_BIT (0x1) → audio_is_valid_format ✓
isRecordThread → audio_is_input_channel(0x10) ✓ (FRONT 是合法输入声道)
Step 4. AudioProfile 匹配(核心)
flags = 0, 不是 MMAP_NOIRQ
→ 走 checkCompatibleAudioProfile
在checkCompatibleProfile()里倒序遍历 3 个AudioProfile:
i=2, Profile 3 (PCM_16_BIT, {48000}, {0x000c, 0x80000003})::
format: 请求 PCM_16_BIT, profile 是 PCM_16_BIT → ✓ 精确匹配
checkCompatibleChannelMask(0x10):
0x000c (AUDIO_CHANNEL_IN_STEREO, positional/AUDIO_CHANNEL_REPRESENTATION_POSITION):
都是 positional → 情况4
交集 popcount = 0, match = 100
特殊分支: STEREO + 请求是 MONO → match = 1000 ★
0x80000003 (index 2ch):
情况3, match = 1
bestMatch = 1000, updatedChannelMask = 0x000c ✓
checkCompatibleSamplingRate(48000):
{48000} 包含 48000 → 精确匹配 ✓
→ Profile 3 匹配成功! 不再继续遍历
匹配到此就结束了,不会继续看 Profile 2 和 1。
Step 5. 标志位匹配
getFlags() = 0x0000 (NONE)
请求 flags = 0 (NONE)
(getFlags() ^ flags) = 0
0 & ~(AUDIO_INPUT_FLAG_FAST) = 0 → 无差异 ✓
Step 6. 回填结果
updatedSamplingRate = 48000 (保持原值,dynamic 不改)
updatedFormat = PCM_16_BIT (保持原值)
updatedChannelMask = 0x000c (从 0x10 MONO 升级为 STEREO)
匹配流程总结
| 检查项 | 结果 |
|---|---|
| 方向 | record ✓ |
| 设备 | USB_HEADSET 在支持列表 ✓ |
| 参数合法性 | format/channelMask 有效 ✓ |
| 音频匹配 | Profile 3 (最精确) ✓ |
| 标志位 | NONE vs NONE ✓ |
全部通过,返回 true。匹配到 Profile 3,声道从 MONO(0x10) 升级为 STEREO(0x000c),因为 STEREO↔MONO 是经典声道转换(得分 1000)。
6. USB 摄像头与 USB 耳麦共存问题分析与解决方案
6.1 问题描述
在Android音频系统中,当同时连接USB摄像头(仅含麦克风)和USB耳麦(含麦克风和扬声器)时,音频功能正常。但拔掉USB摄像头后,USB耳麦的麦克风无法使用,系统无法找到合适的输入IOProfile,导致录音失败。
关键错误日志:
W APM_AudioPolicyManager: getInputForDevice could not find profile for device AUDIO_DEVICE_IN_USB_HEADSET, @:card=1;device=0;, sampling rate 48000, format 0x1, channel mask 0x10, flags 0x20
E AudioFlinger: createRecord() getInputForAttr return error -38
6.2 根本原因分析
原始音频策略配置
在原有的usb_audio_policy_configuration.xml中,USB 模块的配置如下:
<mixPorts>
<mixPort name="usb_device output" role="source"/>
<mixPort name="usb_device input" role="sink"/>
</mixPorts>
<devicePorts>
<devicePort tagName="USB Device In" type="AUDIO_DEVICE_IN_USB_DEVICE" role="source"/>
<devicePort tagName="USB Headset In" type="AUDIO_DEVICE_IN_USB_HEADSET" role="source"/>
</devicePorts>
<routes>
<route type="mix" sink="USB Headset Out" sources="usb_device output"/>
<route type="mix" sink="usb_device input" sources="USB Device In,USB Headset In"/>
</routes>
USB 摄像头(AUDIO_DEVICE_IN_USB_DEVICE)和USB 耳麦(AUDIO_DEVICE_IN_USB_HEADSET)共享同一个mixPort -"usb_device input"。
dumpsys media.audio_policy 命令的结果对比
- 同时接了 USB 摄像头和 USB 耳麦的:
4. Handle: 34; "usb"
- Input MixPorts (1):
1. "usb_device input"; 0x0000 (AUDIO_INPUT_FLAG_NONE)
- Profiles (7):
1. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_DEFAULT (0x0)
AUDIO_ENCAPSULATION_TYPE_NONE
2. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_PCM_16_BIT (0x1)
AUDIO_ENCAPSULATION_TYPE_NONE
3. ""; [dynamic format]; AUDIO_FORMAT_PCM_16_BIT (0x1)
sampling rates: 48000
channel masks: 0x000c, 0x80000003
AUDIO_ENCAPSULATION_TYPE_NONE
4. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_PCM_16_BIT (0x1)
AUDIO_ENCAPSULATION_TYPE_NONE
5. ""; [dynamic format]; AUDIO_FORMAT_PCM_16_BIT (0x1)
sampling rates: 16000
channel masks: 0x000c, 0x0010, 0x80000001
AUDIO_ENCAPSULATION_TYPE_NONE
6. ""; [dynamic format]; AUDIO_FORMAT_PCM_16_BIT (0x1)
sampling rates: 16000
channel masks: 0x000c, 0x0010, 0x80000001
AUDIO_ENCAPSULATION_TYPE_NONE
7. ""; [dynamic format]; AUDIO_FORMAT_PCM_16_BIT (0x1)
sampling rates: 16000
channel masks: 0x000c, 0x0010, 0x80000001
AUDIO_ENCAPSULATION_TYPE_NONE
- Supported devices (4):
1. Port ID: 8; "USB-Audio - 1080P USB Camera"; {AUDIO_DEVICE_IN_USB_DEVICE, @:card=2;device=0;}
Encapsulation modes: 0, metadata types: 0
"USB-Audio - 1080P USB Camera"
2. "USB Device In"; {AUDIO_DEVICE_IN_USB_DEVICE, @:}
Encapsulation modes: 0, metadata types: 0
3. Port ID: 6; "USB-Audio - USB Audio Device"; {AUDIO_DEVICE_IN_USB_HEADSET, @:card=1;device=0;}
Encapsulation modes: 0, metadata types: 0
"USB-Audio - USB Audio Device"
4. "USB Headset In"; {AUDIO_DEVICE_IN_USB_HEADSET, @:}
Encapsulation modes: 0, metadata types: 0
- maxOpenCount: 1; curOpenCount: 0
- maxActiveCount: 1; curActiveCount: 0
- recommendedMuteDurationMs: 0 ms
- 同时接了 USB 摄像头和 USB 耳麦再拔掉 USB 摄像头后的:
4. Handle: 34; "usb"
- Input MixPorts (1):
1. "usb_device input"; 0x0000 (AUDIO_INPUT_FLAG_NONE)
- Profiles (1):
1. ""; [dynamic format][dynamic channels][dynamic rates]; AUDIO_FORMAT_DEFAULT (0x0)
AUDIO_ENCAPSULATION_TYPE_NONE
- Supported devices (3):
1. "USB Device In"; {AUDIO_DEVICE_IN_USB_DEVICE, @:}
Encapsulation modes: 0, metadata types: 0
2. Port ID: 6; "USB-Audio - USB Audio Device"; {AUDIO_DEVICE_IN_USB_HEADSET, @:card=1;device=0;}
Encapsulation modes: 0, metadata types: 0
"USB-Audio - USB Audio Device"
3. "USB Headset In"; {AUDIO_DEVICE_IN_USB_HEADSET, @:}
Encapsulation modes: 0, metadata types: 0
- maxOpenCount: 1; curOpenCount: 0
- maxActiveCount: 1; curActiveCount: 0
- recommendedMuteDurationMs: 0 ms
通过dumpsys media.audio_policy命令的结果对比发现:
- 同时连接摄像头和耳麦时,
usb_device input的Profile列表有7个AudioProfile,支持两种设备 - 拔掉摄像头后,
Profile列表只剩下1个全dynamic的占位AudioProfile
在 checkCompatibleProfile 中的格式匹配条件:
if (formatToCompare == format || // 精确匹配
(checkInexact // true
&& formatToCompare != AUDIO_FORMAT_DEFAULT // ← 这里排除了 DEFAULT!
&& audio_is_linear_pcm(formatToCompare))) // 线性 PCM
当唯一的AudioProfile是 AUDIO_FORMAT_DEFAULT 时:
formatToCompare = AUDIO_FORMAT_DEFAULT
条件1: DEFAULT == PCM_16_BIT → false
条件2: checkInexact=true && DEFAULT != DEFAULT → false ← 短路!
整个条件为 false
→ 跳过该 profile,循环结束
→ return BAD_VALUE 匹配失败!
DEFAULT 格式被显式排除在非精确匹配之外。这是设计意图——DEFAULT 是占位符,表示"格式待定",不应作为兼容匹配的候选。
因为AudioProfile匹配失败,所以"usb_device input"的IOProfile无法被使用,导致录音失败。
为什么会只剩一个占位 AudioProfile
拔掉 USB 摄像头后,系统会触发AudioPolicyManager::checkInputsForDevice() -> IOProfile::clearAudioProfiles()清理动态创建的AudioProfile,只留一个占位AudioProfile。
6.3 解决方案
核心思路
将 USB 耳麦(USB Headset)和 USB 摄像头(USB Device)的音频路径分离,使它们使用独立的mixPort,避免Profile互相影响。
具体修改
对usb_audio_policy_configuration.xml进行以下修改:
Step 1. 添加专用mixPort
<!-- 新增专用于USB耳麦的mixPort -->
<mixPort name="usb_headset output" role="source"/>
<mixPort name="usb_headset input" role="sink"/>
Step 2. 分离路由配置
<!-- 修改前:USB耳麦和USB摄像头共享同一个输出mixPort和输入mixPort -->
<route type="mix" sink="USB Headset Out"
sources="usb_device output"/>
<route type="mix" sink="usb_device input"
sources="USB Device In,USB Headset In"/>
<!-- 修改后:USB耳麦使用专用mixPort -->
<route type="mix" sink="USB Headset Out"
sources="usb_headset output"/>
<!-- 摄像头走原来的 mixPort -->
<route type="mix" sink="usb_device input"
sources="USB Device In"/>
<!-- 耳麦走新的 mixPort,profile 独立管理 -->
<route type="mix" sink="usb_headset input"
sources="USB Headset In"/>
技术原理
Profile 独立性
通过为 USB 耳麦创建独立的usb_headset input mixPort,USB 耳麦的Profile 管理完全独立于USB摄像头:
- USB 摄像头的 Profile 清理不会影响 USB 耳麦
- 两种设备可以同时维护各自的动态 Profile 集合
- 设备插拔事件只影响对应的 mixPort
路由分离 修改后的路由配置确保:
USB Headset In→usb_headset inputUSB Device In→usb_device input
这样即使 USB 摄像头被移除,触发usb_device input的 Profile 清理,usb_headset input中的 Profile 仍然保持不变,USB 耳麦可以继续正常工作。
6.4 小结
问题本质
Android 音频策略的动态 Profile 管理机制在处理共享 mixPort 的多设备场景时存在缺陷。当不同设备共享同一个 mixPort 时,一个设备的 Profile 清理操作会影响其他设备。
解决方案价值
- 架构清晰:为不同类型的 USB 音频设备提供独立的音频路径
- 稳定性提升:避免设备间的相互干扰
- 易于维护:配置清晰,便于后续扩展
- 低风险:通过配置文件修改,无需修改核心代码