.# SA8295P 音频资源管理器 (ResourceManager) 决策逻辑深度解析
在 SA8295P 平台上,ResourceManager (以下简称 RM) 是音频系统的"导航仪"。它通过解析 resourcemanager.xml,将复杂的业务需求转化为具体的硬件配置和算法参数。
一、 XML 加载与解析机制
1. 解析流程:从文本到结构体
RM 使用 Expat (流式 XML 解析库) 在单例初始化时完成加载。
- 入口 :
ResourceManager::init_audio()->ResourceManager::XmlParser(path)。 - 文件路径决策 (源码逻辑) :
系统并非硬编码路径,而是根据声卡名称动态拼接:
c
// 找到加载路径: /vendor/etc/resourcemanager_gvmauto8295_adp_star.xml
int ResourceManager::init_audio()
{
...
getVendorConfigPath(vendor_config_path, sizeof(vendor_config_path));
// 拼接基础名 + 声卡后缀 (如 _gvmauto) + .xml
snprintf(rmngr_xml_file, sizeof(rmngr_xml_file), "%s/%s_%s.xml", ...); // resourcemanager_gvmauto8295_adp_star.xml
...
}
ResourceManager::ResourceManager()
{
...
ResourceManager::init_audio();
ResourceManager::XmlParser(rmngr_xml_file); // 解析 xml
...
}
c
/**
* @brief 解析指定的 XML 配置文件
*
* @param xmlFile XML 文件的完整路径
* @return int 成功返回 0,失败返回相应的错误码(如 -EINVAL)
*/
int ResourceManager::XmlParser(std::string xmlFile)
{
XML_Parser parser; // 定义 expat 解析器句柄
FILE *file = NULL; // 定义文件指针,用于读取 XML 文件
int ret = 0; // 初始化返回值
int bytes_read; // 记录每次从文件中读取的字节数
void *buf = NULL; // 解析器内部使用的缓冲区指针
struct xml_userdata data; // 定义用户自定义数据结构,用于在解析回调函数间传递上下文
memset(&data, 0, sizeof(data)); // 清零初始化数据结构
// 打印日志,记录 XML 解析开始及文件名
PAL_INFO(LOG_TAG, "XML parsing started - file name %s", xmlFile.c_str()); // XML parsing started - file name /vendor/etc/resourcemanager_gvmauto8295_adp_star_a2b.xml
// 以只读模式打开 XML 文件
file = fopen(xmlFile.c_str(), "r");
if(!file) { // 如果文件打开失败(如文件不存在或权限不足)
ret = EINVAL; // 设置错误码为无效参数
PAL_ERR(LOG_TAG, "Failed to open xml file name %s ret %d", xmlFile.c_str(), ret);
goto done; // 跳转到结束位置
}
// 创建一个新的 expat 解析器对象
parser = XML_ParserCreate(NULL);
if (!parser) { // 如果内存不足导致解析器创建失败
ret = -EINVAL;
PAL_ERR(LOG_TAG, "Failed to create XML ret %d", ret);
goto closeFile; // 需要先关闭已打开的文件
}
// 设置解析器的用户数据上下文,以便在回调函数中访问
data.parser = parser;
XML_SetUserData(parser, &data);
// 注册元素处理器:startTag 在遇到标签开始(如 <device>)时调用,endTag 在遇到标签结束(如 </device>)时调用
XML_SetElementHandler(parser, startTag, endTag);
// 注册字符数据处理器:用于处理标签之间的文本内容
XML_SetCharacterDataHandler(parser, snd_data_handler);
// 循环读取文件内容并进行解析
while (1) {
// 从解析器获取一个 1024 字节大小的内部缓冲区
buf = XML_GetBuffer(parser, 1024);
if(buf == NULL) { // 缓冲区分配失败
ret = -EINVAL;
PAL_ERR(LOG_TAG, "XML_Getbuffer failed ret %d", ret);
goto freeParser; // 跳转至释放解析器
}
// 从文件中读取最多 1024 字节数据填充到缓冲区中
bytes_read = fread(buf, 1, 1024, file);
if(bytes_read < 0) { // 文件读取出错
ret = -EINVAL;
PAL_ERR(LOG_TAG, "fread failed ret %d", ret);
goto freeParser;
}
// 将读取到的数据喂给解析器。bytes_read == 0 表示已到达文件末尾(isFinal 为 true)
if(XML_ParseBuffer(parser, bytes_read, bytes_read == 0) == XML_STATUS_ERROR) {
ret = -EINVAL;
// 如果解析过程中发现语法错误,打印错误日志
PAL_ERR(LOG_TAG, "XML ParseBuffer failed for %s file ret %d", xmlFile.c_str(), ret);
goto freeParser;
}
// 如果读取的字节数为 0,说明文件处理完毕,跳出循环
if (bytes_read == 0)
break;
}
freeParser:
// 释放 expat 解析器占用的内存
XML_ParserFree(parser);
closeFile:
// 关闭文件句柄
fclose(file);
done:
// 返回解析结果
return ret;
}
关键逻辑总结:
- 资源管理:函数严格遵循了 C 风格的资源管理模式(goto 模式),确保在任何步骤出错时都能正确释放已分配的内存(解析器)和关闭已打开的文件。
- 分块解析:通过 while 循环和 1024 字节的缓冲区进行流式解析。这种方式对于嵌入式系统(如 SA8295P 平台)非常友好,可以避免一次性将巨大的 XML
文件加载到内存中。 - 事件驱动:该函数本身不处理具体的业务逻辑,而是通过 XML_SetElementHandler 注册的回调函数(startTag, endTag)来构建 PAL
内部的资源树(如设备、流、混音路径等)。
startTag
cpp
// android/android/vendor/qcom/opensource/pal/resource_manager/src/ResourceManager.cpp
void ResourceManager::startTag(void *userdata, const XML_Char *tag_name,
const XML_Char **attr)
{
stream_supported_type type;
struct xml_userdata *data = (struct xml_userdata *)userdata; // 获取透传的用户数据上下文
static std::shared_ptr<SoundTriggerPlatformInfo> st_info = nullptr;
static std::shared_ptr<ACDPlatformInfo> acd_info = nullptr;
// --- 场景 1: 处理嵌套的子模块解析 (SoundTrigger) ---
if (data->is_parsing_sound_trigger) {
if (st_info)
// 如果当前处于语音唤醒配置段,则将解析权移交给 SoundTrigger 模块
st_info->HandleStartTag((const char *)tag_name, (const char **)attr);
return;
}
// --- 场景 2: 处理嵌套的子模块解析 (ACD - 自动内容检测) ---
if (acd_info && data->is_parsing_acd) {
acd_info->HandleStartTag((const char *)tag_name, (const char **)attr);
snd_reset_data_buf(data); // 重置缓冲区,准备接收该标签下的字符数据
return;
}
// --- 场景 3: 处理分组设备配置 ---
if (data->is_parsing_group_device) {
process_group_device_config(data, (const char *)tag_name, (const char **)attr);
snd_reset_data_buf(data);
return;
}
// --- 场景 4: 识别子模块的开始标签 ---
if (!strcmp(tag_name, "sound_trigger_platform_info")) {
data->is_parsing_sound_trigger = true; // 设置标志位,接下来的标签将由 SoundTrigger 处理
st_info = SoundTriggerPlatformInfo::GetInstance();
return;
}
if (!strcmp(tag_name, "acd_platform_info")) {
data->is_parsing_acd = true;
acd_info = ACDPlatformInfo::GetInstance();
return;
}
if (!strcmp(tag_name, "group_device_cfg")) {
data->is_parsing_group_device = true;
return;
}
// --- 场景 5: 处理通用全局配置参数 ---
if (strcmp(tag_name, "device") == 0) {
return; // 仅作为容器标签,跳过
} else if(strcmp(tag_name, "param") == 0) {
processConfigParams(attr); // 处理如 <param key="val" value="1" /> 的通用参数
} else if (strcmp(tag_name, "codec") == 0) {
// 处理蓝牙编解码器信息
processBTCodecInfo(attr, XML_GetSpecifiedAttributeCount(data->parser));
return;
} else if (strcmp(tag_name, "config_gapless") == 0) {
setGaplessMode(attr); // 配置无缝播放
return;
} else if(strcmp(tag_name, "temp_ctrl") == 0) {
processSpkrTempCtrls(attr); // 扬声器温度控制配置
return;
}
// 如果声卡已经解析完成,则不再处理后续标签(避免重复解析)
if (data->card_parsed)
return;
snd_reset_data_buf(data); // 每次遇到新标签,清空之前的字符缓存
// --- 场景 6: 通过标签名识别当前正在解析的对象类型 (设置 tag 标志) ---
if (!strcmp(tag_name, "resource_manager_info")) {
data->tag = TAG_RESOURCE_MANAGER_INFO;
} else if (!strcmp(tag_name, "config_voice")) {
data->tag = TAG_CONFIG_VOICE; // 语音相关配置
} else if (!strcmp(tag_name, "mode_map")) {
data->tag = TAG_CONFIG_MODE_MAP;
} else if (!strcmp(tag_name, "modepair")) {
data->tag = TAG_CONFIG_MODE_PAIR;
process_voicemode_info(attr); // 解析模式配对属性
} else if (!strcmp(tag_name, "gain_db_to_level_mapping")) {
data->tag = TAG_GAIN_LEVEL_MAP; // 增益映射
} else if (!strcmp(tag_name, "gain_level_map")) {
data->tag = TAG_GAIN_LEVEL_PAIR;
process_gain_db_to_level_map(data, attr);
} else if (!strcmp(tag_name, "device_profile")) {
data->tag = TAG_DEVICE_PROFILE; // 设备 Profile
} else if (!strcmp(tag_name, "in-device")) {
data->tag = TAG_IN_DEVICE; // 输入设备定义
} else if (!strcmp(tag_name, "out-device")) {
data->tag = TAG_OUT_DEVICE; // 输出设备定义
} else if (!strcmp(tag_name, "usecase")) {
process_usecase(); // 解析用例信息
data->tag = TAG_USECASE;
} else if (!strcmp(tag_name, "custom-config")) {
process_custom_config(attr); // 自定义 vendor 配置
data->inCustomConfig = 1;
data->tag = TAG_CUSTOMCONFIG;
} else if (!strcmp(tag_name, "control")) {
process_control(attr); // 解析 ALSA Mixer Control
data->tag = TAG_CONTROL;
}
// ... 此处省略了大量类似的 else if 判断 ...
// --- 场景 7: 处理声卡和硬件设备层级关系 ---
if (!strcmp(tag_name, "card"))
data->current_tag = TAG_CARD; // 进入声卡描述段
if (strcmp(tag_name, "pcm-device") == 0) {
type = PCM;
data->current_tag = TAG_DEVICE; // 标记当前正在解析 PCM 设备
} else if (strcmp(tag_name, "compress-device") == 0) {
data->current_tag = TAG_DEVICE; // 标记当前正在解析 Compress 设备
type = COMPRESS;
} else if (strcmp(tag_name, "mixer") == 0) {
data->current_tag = TAG_MIXER; // 进入混音器配置段
} else if (strstr(tag_name, "plugin")) {
data->current_tag = TAG_PLUGIN; // 插件配置
}
// 完整性检查:如果还未找到声卡标签,且当前不是在解析声卡,则忽略这些标签
if (data->current_tag != TAG_CARD && !data->card_found)
return;
}
- 核心逻辑 :
startTag就像一个分拣员,它根据标签名决定将数据送往哪个"仓库"(如deviceInfo或btCodecMap),并利用attr指针提取key和value。
endTag
c
void ResourceManager::endTag(void *userdata, const XML_Char *tag_name)
{
struct xml_userdata *data = (struct xml_userdata *)userdata;
std::shared_ptr<SoundTriggerPlatformInfo> st_info =
SoundTriggerPlatformInfo::GetInstance();
std::shared_ptr<ACDPlatformInfo> acd_info = ACDPlatformInfo::GetInstance();
// --- 场景 1: 结束子模块解析标志 ---
if (!strcmp(tag_name, "sound_trigger_platform_info")) {
data->is_parsing_sound_trigger = false;
return;
}
if (data->is_parsing_sound_trigger) {
// 如果在语音唤醒段内,调用子模块的结束处理逻辑
st_info->HandleEndTag(data, (const char *)tag_name);
return;
}
if (!strcmp(tag_name, "acd_platform_info")) {
data->is_parsing_acd = false;
return;
}
if (data->is_parsing_acd) {
acd_info->HandleEndTag(data, (const char *)tag_name);
snd_reset_data_buf(data);
return;
}
if (!strcmp(tag_name, "group_device_cfg")) {
data->is_parsing_group_device = false;
return;
}
// --- 场景 2: 标签结束时,触发具体业务逻辑的汇总处理 ---
// 许多配置是在读取完所有子标签和字符数据后,在结束标签处一次性存入内存
process_config_voice(data, tag_name); // 处理语音配置汇总
process_device_info(data, tag_name); // 处理设备信息汇总
process_input_streams(data, tag_name); // 处理输入流配置
process_lpi_vote_streams(data, tag_name);
process_config_volume(data, tag_name);
process_config_lpm(data, tag_name);
if (!strcmp(tag_name, "plugin")) {
data->tag = TAG_CONTROL; // 插件解析结束,回退 tag 类型
}
// 状态保护
if (data->card_parsed)
return;
if (data->current_tag != TAG_CARD && !data->card_found)
return;
// --- 场景 3: 处理标签间的文本数据 ---
// 例如 <tag>Value</tag>,此处处理 "Value" 字符数据
snd_process_data_buf(data, tag_name);
snd_reset_data_buf(data); // 处理完后清空缓存
// --- 场景 4: 维护层级状态机 ---
// 当一个容器标签结束时,需要将 current_tag 回退到父节点级别
if (!strcmp(tag_name, "mixer") || !strcmp(tag_name, "pcm-device") || !strcmp(tag_name, "compress-device"))
data->current_tag = TAG_CARD; // 设备解析结束,状态回到"声卡"级别
else if (strstr(tag_name, "plugin") || !strcmp(tag_name, "props"))
data->current_tag = TAG_DEVICE; // 插件结束,回到"设备"级别
else if(!strcmp(tag_name, "card")) {
data->current_tag = TAG_ROOT; // 声卡标签结束
if (data->card_found)
data->card_parsed = true; // 标记主声卡信息已解析完成
}
}
void ResourceManager::endTag(void *userdata, const XML_Char *tag_name)
{
struct xml_userdata *data = (struct xml_userdata *)userdata;
std::shared_ptr<SoundTriggerPlatformInfo> st_info =
SoundTriggerPlatformInfo::GetInstance();
std::shared_ptr<ACDPlatformInfo> acd_info = ACDPlatformInfo::GetInstance();
// --- 场景 1: 结束子模块解析标志 ---
if (!strcmp(tag_name, "sound_trigger_platform_info")) {
data->is_parsing_sound_trigger = false;
return;
}
if (data->is_parsing_sound_trigger) {
// 如果在语音唤醒段内,调用子模块的结束处理逻辑
st_info->HandleEndTag(data, (const char *)tag_name);
return;
}
if (!strcmp(tag_name, "acd_platform_info")) {
data->is_parsing_acd = false;
return;
}
if (data->is_parsing_acd) {
acd_info->HandleEndTag(data, (const char *)tag_name);
snd_reset_data_buf(data);
return;
}
if (!strcmp(tag_name, "group_device_cfg")) {
data->is_parsing_group_device = false;
return;
}
// --- 场景 2: 标签结束时,触发具体业务逻辑的汇总处理 ---
// 许多配置是在读取完所有子标签和字符数据后,在结束标签处一次性存入内存
process_config_voice(data, tag_name); // 处理语音配置汇总
process_device_info(data, tag_name); // 处理设备信息汇总
process_input_streams(data, tag_name); // 处理输入流配置
process_lpi_vote_streams(data, tag_name);
process_config_volume(data, tag_name);
process_config_lpm(data, tag_name);
if (!strcmp(tag_name, "plugin")) {
data->tag = TAG_CONTROL; // 插件解析结束,回退 tag 类型
}
// 状态保护
if (data->card_parsed)
return;
if (data->current_tag != TAG_CARD && !data->card_found)
return;
// --- 场景 3: 处理标签间的文本数据 ---
// 例如 <tag>Value</tag>,此处处理 "Value" 字符数据
snd_process_data_buf(data, tag_name);
snd_reset_data_buf(data); // 处理完后清空缓存
// --- 场景 4: 维护层级状态机 ---
// 当一个容器标签结束时,需要将 current_tag 回退到父节点级别
if (!strcmp(tag_name, "mixer") || !strcmp(tag_name, "pcm-device") || !strcmp(tag_name, "compress-device"))
data->current_tag = TAG_CARD; // 设备解析结束,状态回到"声卡"级别
else if (strstr(tag_name, "plugin") || !strcmp(tag_name, "props"))
data->current_tag = TAG_DEVICE; // 插件结束,回到"设备"级别
else if(!strcmp(tag_name, "card")) {
data->current_tag = TAG_ROOT; // 声卡标签结束
if (data->card_found)
data->card_parsed = true; // 标记主声卡信息已解析完成
}
}
总结
- 权限分发:startTag 和 endTag 识别出 sound_trigger_platform_info 等特殊段落,并将解析任务"外包"给专门的类(如 SoundTriggerPlatformInfo)。
- 状态机控制:使用 data->current_tag 来跟踪 XML 的嵌套层级(Root -> Card -> Device -> Props),确保配置项被归类到正确的硬件实体下。
- 延迟处理:大部分复杂的逻辑(如 process_device_info)放在 endTag 中,因为此时所有必要的属性和子元素都已经读取完毕。
process_config_voice
此函数 ResourceManager::process_config_voice 专门负责解析 XML 配置文件中与 语音通话 (Voice Call) 相关的参数。它通常在 endTag
回调中被调用,用于处理标签之间的文本数据并维护解析状态。
c
/**
* @brief 处理语音通话相关的配置解析
*
* @param data XML 解析的用户自定义数据上下文,包含当前解析状态和缓冲区
* @param tag_name 当前结束的 XML 标签名
*/
void ResourceManager::process_config_voice(struct xml_userdata *data, const XML_Char *tag_name)
{
// 如果语音信息已经解析完成(voice_info_parsed 为真),则直接返回,避免重复处理
if(data->voice_info_parsed)
return;
// 检查缓冲区偏移量。如果 offs <= 0,说明标签之间没有文本内容(例如 <vsid></vsid>)
if (data->offs <= 0)
return;
// 在缓冲区末尾添加字符串结束符 '\0',确保将 data_buf 作为 C 风格字符串处理时的安全性
data->data_buf[data->offs] = '\0';
// 场景 1:如果当前处于 TAG_CONFIG_VOICE 状态(即正在解析 <config_voice> 内部)
if (data->tag == TAG_CONFIG_VOICE) {
// 如果结束的标签是 <vsid> (Voice System ID)
if (strcmp(tag_name, "vsid") == 0) {
std::string vsidvalue(data->data_buf); // 获取缓冲区中的 VSID 字符串
// 将字符串转换为十六进制数值(VSID 通常以 0x10C01000 这种形式存在)
vsidInfo.vsid = convertCharToHex(vsidvalue);
}
// 如果结束的标签是 <loopbackDelay> (回环延迟)
if (strcmp(tag_name, "loopbackDelay") == 0) {
// 将字符串转换为整数,用于通话中的延迟补偿
vsidInfo.loopback_delay = atoi(data->data_buf);
}
// 如果结束的标签是 <maxVolIndex> (最大音量阶数)
if (strcmp(tag_name, "maxVolIndex") == 0) {
// 解析并设置语音通话支持的最大音量级别
max_voice_vol = atoi(data->data_buf);
}
}
// 场景 2:维护解析状态机的层级跳转 (当特定标签结束时回退状态)
// 如果 <modepair> 标签结束
if (!strcmp(tag_name, "modepair")) {
// 状态回退到模式映射层级
data->tag = TAG_CONFIG_MODE_MAP;
}
// 如果 <mode_map> 标签结束
else if (!strcmp(tag_name, "mode_map")) {
// 状态回退到语音配置层级
data->tag = TAG_CONFIG_VOICE;
}
// 如果整个 <config_voice> 根标签结束
else if (!strcmp(tag_name, "config_voice")) {
// 状态回退到资源管理器的顶层信息层级
data->tag = TAG_RESOURCE_MANAGER_INFO;
// 标记语音信息已全部解析完毕,后续不再进入此函数逻辑
data->voice_info_parsed = true;
}
}
关键点说明:
- VSID (Voice System ID):这是高通平台上非常关键的参数,用于标识特定的语音系统实例(如主卡语音、副卡语音等),HAL 需要这个 ID 来通知调制解调器 (Modem)
开启对应的音频路径。 - 状态机迁移:该函数通过修改 data->tag 来实现 XML 树状结构的"爬升"。当一个子配置段(如 modepair)解析结束,它会将 tag
改回父节点的值,确保后续标签能在正确的上下文中被识别。 - 延迟转换:数值转换(convertCharToHex, atoi)发生在 endTag 时刻,因为此时 data_buf 已经收集到了完整的标签内文本。
process_device_info
此函数 ResourceManager::process_device_info 是 ResourceManager 中最核心的解析逻辑之一,负责从 XML 中提取音频设备(输入/输出)及其关联的用例(Usecase)和自定义配置。它建立并填充了 PAL 内部的设备信息数据库。
c
/**
* @brief 处理设备及其子项(用例、回声参考、自定义配置)的详细解析
*
* @param data XML 解析上下文
* @param tag_name 当前结束的标签名
*/
void ResourceManager::process_device_info(struct xml_userdata *data, const XML_Char *tag_name)
{
// 默认设备结构体,初始设置为 PCM 16位小端格式
struct deviceIn dev = {
.bitFormatSupported = PAL_AUDIO_FMT_PCM_S16_LE,
};
int size = 0 , sizeusecase = 0, sizecustomconfig = 0;
// 基础检查:如果没有字符数据或已经解析完成,直接返回
if (data->offs <= 0)
return;
data->data_buf[data->offs] = '\0'; // 字符串截断
if (data->resourcexml_parsed)
return;
// --- 场景 1: 处理设备基础属性 (<in-device> 或 <out-device> 标签内) ---
if ((data->tag == TAG_IN_DEVICE) || (data->tag == TAG_OUT_DEVICE)) {
if (!strcmp(tag_name, "id")) {
// 遇到 <id> 标签(如 PAL_DEVICE_OUT_SPEAKER),通过查找表 (LUT) 转换为 ID
std::string deviceName(data->data_buf);
dev.deviceId = deviceIdLUT.at(deviceName);
// 将新设备存入 deviceInfo 列表,后续属性将更新这最后一个元素
deviceInfo.push_back(dev);
} else if (!strcmp(tag_name, "back_end_name")) {
// 解析后端接口名(如 'speaker'),对应音频 DSP 的端口
std::string backendname(data->data_buf);
size = deviceInfo.size() - 1;
updateBackEndName(deviceInfo[size].deviceId, backendname);
} else if (!strcmp(tag_name, "max_channels")) {
// 该设备支持的最大声道数
size = deviceInfo.size() - 1;
deviceInfo[size].max_channel = atoi(data->data_buf);
} else if (!strcmp(tag_name, "channels")) {
// 该设备的默认声道数
size = deviceInfo.size() - 1;
deviceInfo[size].channel = atoi(data->data_buf);
} else if (!strcmp(tag_name, "samplerate")) {
// 该设备的采样率
size = deviceInfo.size() - 1;
deviceInfo[size].samplerate = atoi(data->data_buf);
} else if (!strcmp(tag_name, "snd_device_name")) {
// 解析 ALSA Sound Device 名称(用于 Mixer 控制)
size = deviceInfo.size() - 1;
std::string snddevname(data->data_buf);
deviceInfo[size].sndDevName = snddevname;
updateSndName(deviceInfo[size].deviceId, snddevname);
} else if (!strcmp(tag_name, "speaker_protection_enabled")) {
// 全局开关:是否开启扬声器保护
if (atoi(data->data_buf))
isSpeakerProtectionEnabled = true;
} else if (!strcmp(tag_name, "ext_ec_ref_enabled")) {
// 该设备是否支持外部回声消除参考信号
size = deviceInfo.size() - 1;
deviceInfo[size].isExternalECRefEnabled = atoi(data->data_buf);
} else if (!strcmp(tag_name, "supported_bit_format")) {
// 解析设备支持的位深格式(S16, S24, S32)
size = deviceInfo.size() - 1;
if(!strcmp(data->data_buf, "PAL_AUDIO_FMT_PCM_S24_3LE"))
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S24_3LE;
else if(!strcmp(data->data_buf, "PAL_AUDIO_FMT_PCM_S24_LE"))
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S24_LE;
else if(!strcmp(data->data_buf, "PAL_AUDIO_FMT_PCM_S32_LE"))
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S32_LE;
else
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S16_LE;
}
}
// --- 场景 2: 处理设备关联的 Usecase (用例) 覆盖属性 ---
else if (data->tag == TAG_USECASE) {
if (!strcmp(tag_name, "name")) {
// 用例名称(如 PAL_STREAM_LOW_LATENCY),将其转换为 ID 存入当前设备的 usecase 列表中
std::string userIdname(data->data_buf);
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
deviceInfo[size].usecase[sizeusecase].type = usecaseIdLUT.at(userIdname);
} else if (!strcmp(tag_name, "sidetone_mode")) {
// 侧音 (Sidetone) 模式配置
std::string mode(data->data_buf);
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
deviceInfo[size].usecase[sizeusecase].sidetoneMode = sidetoneModetoId.at(mode);
} else if (!strcmp(tag_name, "samplerate")) {
// 该用例下该设备的特定采样率(覆盖默认值)
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
deviceInfo[size].usecase[sizeusecase].samplerate = atoi(data->data_buf);
}
// ... 其他属性(channels, priority, bit_width)逻辑类似 ...
}
// --- 场景 3: 处理 EC Ref (回声消除参考设备) ---
else if (data->tag == TAG_ECREF) {
if (!strcmp(tag_name, "id")) {
// 当录音设备需要 EC 时,指定哪个播放设备作为参考源
std::string rxDeviceName(data->data_buf);
pal_device_id_t rxDeviceId = deviceIdLUT.at(rxDeviceName);
size = deviceInfo.size() - 1;
deviceInfo[size].rx_dev_ids.push_back(rxDeviceId); // 将参考设备 ID 加入列表
}
}
// --- 场景 4: 处理 Custom Config (自定义 Vendor 配置) ---
else if (data->tag == TAG_CUSTOMCONFIG) {
// 这部分处理在特定用例下的更深层级定制,如不同场景下的采样率或声道
if (!strcmp(tag_name, "snd_device_name")) {
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
sizecustomconfig = deviceInfo[size].usecase[sizeusecase].config.size() - 1;
deviceInfo[size].usecase[sizeusecase].config[sizecustomconfig].sndDevName = data->data_buf;
}
// ... 其他属性逻辑类似,主要用于多级配置覆盖 ...
}
// --- 状态维护:标签结束时回退解析层级 ---
if (!strcmp(tag_name, "usecase")) {
data->tag = TAG_IN_DEVICE; // 用例结束,回到设备层级
} else if (!strcmp(tag_name, "in-device") || !strcmp(tag_name, "out-device")) {
data->tag = TAG_DEVICE_PROFILE; // 设备结束,回到 Profile 层级
} else if (!strcmp(tag_name, "resource_manager_info")) {
data->tag = TAG_RESOURCE_ROOT; // 整个 RM 信息结束
data->resourcexml_parsed = true;
} else if (!strcmp(tag_name, "custom-config")) {
data->tag = TAG_USECASE; // 自定义配置结束,回到用例层级
data->inCustomConfig = 0;
}
}
/**
* @brief 处理设备及其子项(用例、回声参考、自定义配置)的详细解析
*
* @param data XML 解析上下文
* @param tag_name 当前结束的标签名
*/
void ResourceManager::process_device_info(struct xml_userdata *data, const XML_Char *tag_name)
{
// 默认设备结构体,初始设置为 PCM 16位小端格式
struct deviceIn dev = {
.bitFormatSupported = PAL_AUDIO_FMT_PCM_S16_LE,
};
int size = 0 , sizeusecase = 0, sizecustomconfig = 0;
// 基础检查:如果没有字符数据或已经解析完成,直接返回
if (data->offs <= 0)
return;
data->data_buf[data->offs] = '\0'; // 字符串截断
if (data->resourcexml_parsed)
return;
// --- 场景 1: 处理设备基础属性 (<in-device> 或 <out-device> 标签内) ---
if ((data->tag == TAG_IN_DEVICE) || (data->tag == TAG_OUT_DEVICE)) {
if (!strcmp(tag_name, "id")) {
// 遇到 <id> 标签(如 PAL_DEVICE_OUT_SPEAKER),通过查找表 (LUT) 转换为 ID
std::string deviceName(data->data_buf);
dev.deviceId = deviceIdLUT.at(deviceName);
// 将新设备存入 deviceInfo 列表,后续属性将更新这最后一个元素
deviceInfo.push_back(dev);
} else if (!strcmp(tag_name, "back_end_name")) {
// 解析后端接口名(如 'speaker'),对应音频 DSP 的端口
std::string backendname(data->data_buf);
size = deviceInfo.size() - 1;
updateBackEndName(deviceInfo[size].deviceId, backendname);
} else if (!strcmp(tag_name, "max_channels")) {
// 该设备支持的最大声道数
size = deviceInfo.size() - 1;
deviceInfo[size].max_channel = atoi(data->data_buf);
} else if (!strcmp(tag_name, "channels")) {
// 该设备的默认声道数
size = deviceInfo.size() - 1;
deviceInfo[size].channel = atoi(data->data_buf);
} else if (!strcmp(tag_name, "samplerate")) {
// 该设备的采样率
size = deviceInfo.size() - 1;
deviceInfo[size].samplerate = atoi(data->data_buf);
} else if (!strcmp(tag_name, "snd_device_name")) {
// 解析 ALSA Sound Device 名称(用于 Mixer 控制)
size = deviceInfo.size() - 1;
std::string snddevname(data->data_buf);
deviceInfo[size].sndDevName = snddevname;
updateSndName(deviceInfo[size].deviceId, snddevname);
} else if (!strcmp(tag_name, "speaker_protection_enabled")) {
// 全局开关:是否开启扬声器保护
if (atoi(data->data_buf))
isSpeakerProtectionEnabled = true;
} else if (!strcmp(tag_name, "ext_ec_ref_enabled")) {
// 该设备是否支持外部回声消除参考信号
size = deviceInfo.size() - 1;
deviceInfo[size].isExternalECRefEnabled = atoi(data->data_buf);
} else if (!strcmp(tag_name, "supported_bit_format")) {
// 解析设备支持的位深格式(S16, S24, S32)
size = deviceInfo.size() - 1;
if(!strcmp(data->data_buf, "PAL_AUDIO_FMT_PCM_S24_3LE"))
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S24_3LE;
else if(!strcmp(data->data_buf, "PAL_AUDIO_FMT_PCM_S24_LE"))
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S24_LE;
else if(!strcmp(data->data_buf, "PAL_AUDIO_FMT_PCM_S32_LE"))
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S32_LE;
else
deviceInfo[size].bitFormatSupported = PAL_AUDIO_FMT_PCM_S16_LE;
}
}
// --- 场景 2: 处理设备关联的 Usecase (用例) 覆盖属性 ---
else if (data->tag == TAG_USECASE) {
if (!strcmp(tag_name, "name")) {
// 用例名称(如 PAL_STREAM_LOW_LATENCY),将其转换为 ID 存入当前设备的 usecase 列表中
std::string userIdname(data->data_buf);
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
deviceInfo[size].usecase[sizeusecase].type = usecaseIdLUT.at(userIdname);
} else if (!strcmp(tag_name, "sidetone_mode")) {
// 侧音 (Sidetone) 模式配置
std::string mode(data->data_buf);
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
deviceInfo[size].usecase[sizeusecase].sidetoneMode = sidetoneModetoId.at(mode);
} else if (!strcmp(tag_name, "samplerate")) {
// 该用例下该设备的特定采样率(覆盖默认值)
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
deviceInfo[size].usecase[sizeusecase].samplerate = atoi(data->data_buf);
}
// ... 其他属性(channels, priority, bit_width)逻辑类似 ...
}
// --- 场景 3: 处理 EC Ref (回声消除参考设备) ---
else if (data->tag == TAG_ECREF) {
if (!strcmp(tag_name, "id")) {
// 当录音设备需要 EC 时,指定哪个播放设备作为参考源
std::string rxDeviceName(data->data_buf);
pal_device_id_t rxDeviceId = deviceIdLUT.at(rxDeviceName);
size = deviceInfo.size() - 1;
deviceInfo[size].rx_dev_ids.push_back(rxDeviceId); // 将参考设备 ID 加入列表
}
}
// --- 场景 4: 处理 Custom Config (自定义 Vendor 配置) ---
else if (data->tag == TAG_CUSTOMCONFIG) {
// 这部分处理在特定用例下的更深层级定制,如不同场景下的采样率或声道
if (!strcmp(tag_name, "snd_device_name")) {
size = deviceInfo.size() - 1;
sizeusecase = deviceInfo[size].usecase.size() - 1;
sizecustomconfig = deviceInfo[size].usecase[sizeusecase].config.size() - 1;
deviceInfo[size].usecase[sizeusecase].config[sizecustomconfig].sndDevName = data->data_buf;
}
// ... 其他属性逻辑类似,主要用于多级配置覆盖 ...
}
// --- 状态维护:标签结束时回退解析层级 ---
if (!strcmp(tag_name, "usecase")) {
data->tag = TAG_IN_DEVICE; // 用例结束,回到设备层级
} else if (!strcmp(tag_name, "in-device") || !strcmp(tag_name, "out-device")) {
data->tag = TAG_DEVICE_PROFILE; // 设备结束,回到 Profile 层级
} else if (!strcmp(tag_name, "resource_manager_info")) {
data->tag = TAG_RESOURCE_ROOT; // 整个 RM 信息结束
data->resourcexml_parsed = true;
} else if (!strcmp(tag_name, "custom-config")) {
data->tag = TAG_USECASE; // 自定义配置结束,回到用例层级
data->inCustomConfig = 0;
}
}
关键逻辑总结:
- 分层覆盖机制:代码展示了 PAL 如何处理配置优先级:Device 默认属性 < Usecase 特定属性 < Custom-Config 自定义属性。解析器通过 deviceInfo.size() - 1
始终操作当前正在处理的最顶层对象。 - 多对多映射:通过 TAG_ECREF 的解析,建立了输入设备与多个参考输出设备之间的映射关系,这对于通话中的回声消除至关重要。
- 安全性:对 bit_width 等关键参数使用了 isBitWidthSupported 检查,若 XML 配置非法则强制回退到 BITWIDTH_16 默认值,体现了代码的健壮性。
- 硬件关联:updateBackEndName 和 updateSndName 直接将 XML 配置与底层的 ALSA 端口和 Mixer 控件关联起来,是连接配置与驱动的关键纽带
process_input_streams
此函数 ResourceManager::process_input_streams 专门负责解析 XML 配置文件中关于输入流(TX Stream)的回声消除(EC)参考策略。它定义了哪些播放流(RX
Stream)不应该作为特定录音流的回声消除参考源。
c
/**
* @brief 处理输入流(TX)及其回声消除(EC)参考策略的解析
*
* @param data XML 解析上下文,包含缓冲区和当前标签状态
* @param tag_name 当前结束的 XML 标签名
*/
void ResourceManager::process_input_streams(struct xml_userdata *data, const XML_Char *tag_name)
{
struct tx_ecinfo txecinfo = {}; // 定义临时结构体,用于存储单个输入流的 EC 信息
int type = 0; // 用于存储转换后的流类型 ID
int size = -1; // 用于记录 txEcInfo 向量的索引
// 基础检查:如果缓冲区没有内容,则直接返回
if (data->offs <= 0)
return;
// 在缓冲区末尾添加字符串结束符,确保 data_buf 是一个合法的字符串
data->data_buf[data->offs] = '\0';
// 如果整个资源 XML 已标记为解析完成,则不再处理
if (data->resourcexml_parsed)
return;
// --- 场景 1: 处理输入流定义 (<in_stream> 标签内部) ---
if (data->tag == TAG_INSTREAM) {
// 如果结束的标签是 <name>(例如 <name>PAL_STREAM_LOW_LATENCY</name>)
if (!strcmp(tag_name, "name")) {
std::string userIdname(data->data_buf); // 获取标签内的流名称字符串
// 通过查找表 (LUT) 将名称转换为 PAL 内部枚举 ID (pal_stream_type_t)
txecinfo.tx_stream_type = usecaseIdLUT.at(userIdname);
// 将该流信息压入全局的 txEcInfo 向量中,后续操作将针对这最后一个元素进行
txEcInfo.push_back(txecinfo);
PAL_DBG(LOG_TAG, "name %d", txecinfo.tx_stream_type); // 打印调试信息
}
}
// --- 场景 2: 处理回声消除参考策略 (<ec_ref> 标签内部) ---
else if (data->tag == TAG_ECREF) {
// 如果结束的标签是 <disabled_stream>
// 这表示对于当前正在解析的 TX 流,该特定的 RX 流不应作为 EC 参考源
if (!strcmp(tag_name, "disabled_stream")) {
std::string userIdname(data->data_buf); // 获取要禁用的 RX 流名称
type = usecaseIdLUT.at(userIdname); // 转换为 ID
size = txEcInfo.size() - 1; // 获取当前正在配置的 TX 流在向量中的索引
// 将该 RX 流 ID 加入到该 TX 流的禁用列表中
txEcInfo[size].disabled_rx_streams.push_back(type);
PAL_DBG(LOG_TAG, "ecref %d", type);
}
}
// --- 场景 3: 状态机维护(标签结束时回退解析层级) ---
// 根据结束的标签名,将 data->tag 更新为父级状态,确保 XML 解析的层级正确性
if (!strcmp(tag_name, "in_streams")) {
data->tag = TAG_INSTREAMS; // 整个输入流配置段结束
} else if (!strcmp(tag_name, "in_stream")) {
data->tag = TAG_INSTREAM; // 单个输入流配置项结束
} else if (!strcmp(tag_name, "policies")) {
data->tag = TAG_POLICIES; // 策略段结束
} else if (!strcmp(tag_name, "ec_ref")) {
data->tag = TAG_ECREF; // EC 参考段结束
} else if (!strcmp(tag_name, "resource_manager_info")) {
// 资源管理器信息根标签结束,标记整个 XML 解析完成
data->tag = TAG_RESOURCE_ROOT;
data->resourcexml_parsed = true;
}
}
关键逻辑总结:
- EC 策略控制:在高通音频架构中,默认情况下,录音(TX)会参考播放(RX)的信号进行回声消除。但某些特定的流(如低功耗流或特定提示音)不适合作为参考信号。此函数
通过解析 <disabled_stream>,让开发者可以在 XML 中灵活配置这些排除规则。 - 数据结构填充:解析结果最终存储在 ResourceManager 的成员变量 txEcInfo 中。这是一个 std::vector
<struct tx_ecinfo>,每个元素对应一个 TX 流及其相关的 EC 禁用名单。 - 层级识别:函数利用 data->tag(状态位)来区分当前解析的是流的"名称"还是流的"EC 策略",防止在 XML 结构复杂时出现误解析。
process_lpi_vote_streams
此函数 ResourceManager::process_lpi_vote_streams 专门负责解析 XML 配置文件中关于 低功耗岛 (LPI - Low Power Island) 投票流 的配置。在 Qualcomm
的音频架构中,某些音频流可以"投票"允许 DSP 进入或退出低功耗模式,以达到省电的目的。
c
/**
* @brief 处理 LPI (低功耗模式) 投票流的解析
*
* @param data XML 解析上下文,包含字符缓冲区和当前解析状态
* @param tag_name 当前结束的 XML 标签名
*/
void ResourceManager::process_lpi_vote_streams(struct xml_userdata *data,
const XML_Char *tag_name)
{
// 基础检查:如果缓冲区为空(offs <= 0)或者整个 XML 已解析完成,则直接返回
if (data->offs <= 0 || data->resourcexml_parsed)
return;
// 在缓冲区末尾添加字符串结束符 '\0',确保将 data_buf 视为合法的 C 风格字符串
data->data_buf[data->offs] = '\0';
// --- 场景 1: 处理流类型定义 (<stream_type> 标签内部) ---
// 如果当前解析状态标记为 TAG_LPI_VOTE_STREAM
if (data->tag == TAG_LPI_VOTE_STREAM) {
// 从缓冲区获取流名称字符串(如 "PAL_STREAM_LOW_LATENCY")
std::string stream_name(data->data_buf);
PAL_DBG(LOG_TAG, "Stream name to be added : :%s", stream_name.c_str());
// 通过查找表 (usecaseIdLUT) 将字符串形式的流名转换为 PAL 内部的枚举 ID (uint32_t)
uint32_t st = usecaseIdLUT.at(stream_name);
// 将转换后的流 ID 存入 ResourceManager 的成员向量 lpi_vote_streams_ 中
// 这一步告诉系统,该类型的流在运行时会参与低功耗状态的决策(投票)
lpi_vote_streams_.push_back(st);
PAL_DBG(LOG_TAG, "Stream type added : %d", st);
}
// --- 场景 2: 状态机维护(标签结束时回退解析层级) ---
// 如果结束的标签是 <stream_type>
if (!strcmp(tag_name, "stream_type")) {
// 状态回退到睡眠监控 LPI 流的父层级
data->tag = TAG_SLEEP_MONITOR_LPI_STREAM;
}
// 如果结束的是整个 <low_power_vote_streams> 根配置段
else if (!strcmp(tag_name, "low_power_vote_streams")) {
// 状态回退到资源管理器的顶层信息层级
data->tag = TAG_RESOURCE_MANAGER_INFO;
}
}
关键逻辑总结:
- LPI (Low Power Island):这是高通音频 DSP 上的一个低功耗子系统。为了平衡功耗和性能,HAL 需要知道哪些类型的音频流(如语音唤醒、低延迟播放等)能够支持在
LPI 模式下运行。 - 配置驱动:通过 XML 配置这些流,而不是在代码中硬编码,使得针对不同产品(如车载主机 vs 手机)优化功耗策略变得非常灵活。
- 流类型转换:利用 usecaseIdLUT 查找表,将易读的 XML 字符串(PAL_STREAM_COMPRESS 等)转换为系统可识别的数值常量。
- 向量存储:最终所有被解析出来的流 ID 都被保存在 lpi_vote_streams_ 中,供后续音频链路启动时判断是否需要向电源管理模块进行 LPI 投票。
process_config_volume
此函数 ResourceManager::process_config_volume 负责解析 XML 配置文件中关于 音量控制策略 的设置。在 Qualcomm 的音频架构中,音量可以通过传统的混音器(Mixer)控制,也可以通过向 DSP 发送特定的参数(Set Param)来控制。该函数定义了哪些音频流采用后一种方式。
c
/**
* @brief 处理音量控制配置及支持 SetParam 方式的流列表解析
*
* @param data XML 解析上下文,包含字符缓冲区和当前解析状态
* @param tag_name 当前结束的 XML 标签名
*/
void ResourceManager::process_config_volume(struct xml_userdata *data, const XML_Char *tag_name)
{
// 基础检查:如果缓冲区为空或 XML 已解析完成,则直接返回
if (data->offs <= 0 || data->resourcexml_parsed)
return;
// 在缓冲区末尾添加字符串结束符 '\0',确保将 data_buf 视为合法的 C 风格字符串
data->data_buf[data->offs] = '\0';
// --- 场景 1: 处理音量全局配置 (<config_volume> 标签内部) ---
if (data->tag == TAG_CONFIG_VOLUME) {
// 如果结束的标签是 <use_volume_set_param>
// 该值决定了系统是否启用通过 set_param 接口(而非 Mixer)来设置音量
if (strcmp(tag_name, "use_volume_set_param") == 0) {
volumeSetParamInfo_.isVolumeUsingSetParam = atoi(data->data_buf);
}
}
// --- 场景 2: 处理支持 SetParam 的具体流类型 (<supported_stream> 标签内部) ---
if (data->tag == TAG_CONFIG_VOLUME_SET_PARAM_SUPPORTED_STREAM) {
// 获取流名称字符串(例如 "PAL_STREAM_MUSIC")
std::string stream_name(data->data_buf);
PAL_DBG(LOG_TAG, "[PKU]Stream name to be added : :%s", stream_name.c_str());
// 通过查找表将字符串转换为 PAL 内部枚举 ID
uint32_t st = usecaseIdLUT.at(stream_name);
// 将该流 ID 存入 volumeSetParamInfo_ 结构体的 streams_ 向量中
// 系统运行时会根据此列表判断该流是否应使用特殊的音量设置路径
volumeSetParamInfo_.streams_.push_back(st);
PAL_DBG(LOG_TAG, "[PKU]Stream type added for volume set param : %d", st);
}
// --- 场景 3: 状态机维护(标签结束时回退解析层级) ---
// 如果单个流定义标签结束
if (!strcmp(tag_name, "supported_stream")) {
// 状态回退到"支持的流列表"容器层级
data->tag = TAG_CONFIG_VOLUME_SET_PARAM_SUPPORTED_STREAMS;
}
// 如果整个流列表容器结束
else if (!strcmp(tag_name, "supported_streams")) {
// 状态回退到音量配置根层级
data->tag = TAG_CONFIG_VOLUME;
}
// 如果整个音量配置段结束
else if (!strcmp(tag_name, "config_volume")) {
// 状态回退到资源管理器顶层信息层级
data->tag = TAG_RESOURCE_MANAGER_INFO;
}
}
关键逻辑总结:
- 控制策略切换:isVolumeUsingSetParam 是一个关键开关。在某些高通平台上,为了实现更精细的音量曲线控制或硬件限制,必须通过向音频 DSP 的特定模块发送参数(Set
Param)来调节音量,而不是操作传统的底层内核 Mixer 控件。 - 针对性配置:并非所有流都需要这种特殊处理。通过 streams_ 列表,可以精细地为特定流(如 PAL_STREAM_MUSIC)开启该功能,而保持其他流(如
PAL_STREAM_VOICE_CALL)使用默认方式。 - 解析稳定性:使用了典型的 XML 容器模式解析,通过维护 data->tag 状态位,确保只有在正确的嵌套层级下才会将流 ID 存入 volumeSetParamInfo_.streams_。
process_config_lpm
此函数 ResourceManager::process_config_lpm 专门负责解析 XML 配置文件中关于 低功耗模式 (LPM - Low Power Mode) 禁用策略
的设置。在音频播放过程中,系统通常会为了省电进入低功耗状态,但某些高性能流(如超高清音频)可能需要禁用 LPM
以保证音质或低延迟,该函数定义了这些特定的流列表。
c
/**
* @brief 处理低功耗模式 (LPM) 的禁用配置及受影响的流列表解析
*
* @param data XML 解析上下文,包含字符缓冲区和当前解析状态
* @param tag_name 当前结束的 XML 标签名
*/
void ResourceManager::process_config_lpm(struct xml_userdata *data, const XML_Char *tag_name)
{
// 基础检查:如果缓冲区为空或整个 XML 已标记为解析完成,则直接返回
if (data->offs <= 0 || data->resourcexml_parsed)
return;
// 在缓冲区末尾添加字符串结束符 '\0',确保将 data_buf 视为合法的 C 风格字符串
data->data_buf[data->offs] = '\0';
// --- 场景 1: 处理 LPM 全局禁用开关 (<config_lpm> 标签内部) ---
if (data->tag == TAG_CONFIG_LPM) {
// 如果结束的标签是 <use_disable_lpm>
// 该标志位用于决定是否在全局范围内对特定音频流应用禁用 LPM 的策略
if (strcmp(tag_name, "use_disable_lpm") == 0) {
disableLpmInfo_.isDisableLpm = atoi(data->data_buf);
}
}
// --- 场景 2: 处理受此策略影响的具体流类型 (<lpm_supported_stream> 标签内部) ---
if (data->tag == TAG_CONFIG_LPM_SUPPORTED_STREAM) {
// 获取 XML 中配置的流名称(例如 "PAL_STREAM_ULTRA_LOW_LATENCY")
std::string stream_name(data->data_buf);
PAL_DBG(LOG_TAG, "[PKU]Stream name to be added : :%s", stream_name.c_str());
// 通过查找表将字符串形式的流名转换为 PAL 内部枚举 ID
uint32_t st = usecaseIdLUT.at(stream_name);
// 将转换后的流 ID 存入 disableLpmInfo_ 结构体的 streams_ 向量中
// 当这些流在运行时被激活,HAL 会根据此列表通知驱动程序不要进入低功耗模式
disableLpmInfo_.streams_.push_back(st);
PAL_DBG(LOG_TAG, "[PKU]Stream type added for disable lpm : %d", st);
}
// --- 场景 3: 状态机维护(标签结束时回退解析层级) ---
// 如果单个流定义标签结束
if (!strcmp(tag_name, "lpm_supported_stream")) {
// 状态回退到"LPM 支持的流列表"容器层级
data->tag = TAG_CONFIG_LPM_SUPPORTED_STREAMS;
}
// 如果整个流列表容器结束
else if (!strcmp(tag_name, "lpm_supported_streams")) {
// 状态回退到 LPM 配置根层级
data->tag = TAG_CONFIG_LPM;
}
// 如果整个 LPM 配置段结束
else if (!strcmp(tag_name, "config_lpm")) {
// 状态回退到资源管理器顶层信息层级
data->tag = TAG_RESOURCE_MANAGER_INFO;
}
}
关键逻辑总结:
- 性能与功耗平衡:LPM (Low Power Mode) 能够显著降低功耗,但有时会导致音频链路唤醒延迟或处理能力受限。通过 isDisableLpm 开关,HAL
可以灵活控制是否在这些特定场景下优先保证性能。 - 细粒度控制:通过 disableLpmInfo_.streams_ 列表,开发者可以精细地指定哪些音频流(如 VOIP 或 高保真音乐)需要强制维持在高功耗(高性能)状态。
- 解析逻辑一致性:该函数遵循了 ResourceManager 统一的状态机解析模式,通过维护 data->tag 来确保在复杂的 XML 树结构中能够正确地定位和填充配置数据。
2. 解析时序图
Data Structures (Memory) Expat Parser ResourceManager Data Structures (Memory) Expat Parser ResourceManager alt [匹配到 <in-device>] [匹配到 <kvpair>] loop [逐行解析] XML 解析完成,数据结构已就绪 getInstance() & init_audio() XML_ParserCreate & XML_Parse startTag(tag_name, attrs) 创建 deviceIn 实例并压入 deviceInfo 向量 解析 Key/Value 并存入当前设备 Metadata endTag()
二、 核心标签详解与决策逻辑
1. <config_params>:全局战略枢纽
- 语义:定义音频框架的底层运行模式。
- 子标签说明 :
native_audio_mode: 定义混合路径。multiple_mix_dsp指示所有并发流都在 DSP 内部混合,而非 CPU。max_sessions: 规定系统吞吐量。设为128是为了应对座舱内多路输出(主驾、副驾、后排)的复杂需求。
- 决策案例:当 App 尝试开启第 129 个音频流时,RM 会直接拒绝请求,防止 DSP 内存溢出。
2. <bt_codecs>:蓝牙协议分流器
- 语义:蓝牙编解码器格式与动态库的映射表。
- 决策逻辑 :RM 通过维护一个
std::map,在蓝牙耳机连接完成 SDP 协商后,根据协商出的codec_format(如 AAC)立即定位并加载对应的.so插件。 - 硬件背景:由于不同格式(如 LDAC vs SBC)的 CPU 占用和算法私密性不同,这种映射确保了灵活性。
3. <controls>:功能插件分配器
- 语义:定义"音量、延迟、缓冲区"等控制逻辑的具体实现。
- 决策逻辑 :
PLUGIN_CONTROL_VOLUME: 决定了音量调节是线性的还是对数的。plugin_usecase: 过滤器机制。只有列表中的流(如PAL_STREAM_PLAYBACK_BUS)才会被该插件接管。
- 案例 :当操作车载总线音频(Bus Audio)音量时,RM 会根据此标签加载
lib_default_plugin_controls.so进行平滑增益。
4. <config_voice>:通话系统映射
- 语义:定义语音通话的 VSID (Voice System ID) 和模式映射。
- 决策案例 :当用户从 4G 通话切换到 5G 宽带通话时,
mode_map中的modepair会引导系统切换到更高采样率的 DSP 通道。
5. <device_profile>:设备能力总览
- 语义 :容器标签,包含所有输入和输出设备的画像。它是
deviceInfo数据结构的主要来源。
6. <in-device>:输入设备(麦克风)精细决策
- 核心属性 :
back_end_name: 物理硬件端口。TDM-LPAIF_AUD-TX-PRIMARY对应芯片上的具体 TDM 总线。devicePP-metadata: 算法灵魂 。通过 KV 对(如0xAD000000)告诉 DSP 应该加载哪种降噪算法。
- 案例 :Handset Mic 在
PAL_STREAM_VOICE_UI场景下,会加载0xAD000001。这决定了语音助理在背景音乐嘈杂的情况下,依然能精准识别唤醒词。
7. <out-device>:输出设备(扬声器/耳机)执行决策
- 核心属性 :
speaker_protection_enabled: 安全决策。若设为1,RM 会在启动该路径时,强制打通电流/电压反馈(VI Sense)通路。speaker_mono_right: 硬件适配。定义了在单声道输出场景下,使用左侧还是右侧功放。
8. <in_streams>:流策略与互斥准则
- 语义 :定义输入流在并发时的干扰规避策略,核心是 EC Ref (回声参考)。
- 决策逻辑 :
- 当录音流启动时,RM 检查其
policies。 - 如果
disabled_stream包含PAL_STREAM_LOW_LATENCY,则意味着系统在计算回声消除时,会自动"无视"那些极短的音效(如按键音),以防止算法因 these 突发声音而崩溃。
- 当录音流启动时,RM 检查其
三、 实战演练:播放媒体音时如何看此表?
假设 Android 用户打开网易云音乐播放一首歌曲。以下是 ResourceManager 的决策全过程:
Step 1: 识别流类型 (Usecase)
Android 框架通过 PAL 接口请求开启一个流。RM 识别到流类型为:
PAL_STREAM_DEEP_BUFFER(典型媒体音流)。
Step 2: 匹配控制插件 (<controls>)
RM 在 ControlInfo 向量中查找该 Usecase。
- 匹配结果 :在
PLUGIN_CONTROL_VOLUME标签下找到了PAL_STREAM_DEEP_BUFFER。 - 决策 :系统决定使用
lib_default_plugin_controls.so插件来管理这首歌的音量调节。
Step 3: 确定输出物理后端 (<out-device>)
假设当前输出设备是扬声器(PAL_DEVICE_OUT_SPEAKER)。
- 查找 XML :在
<device_profile>-><out-device>中找到id为该值的项。 - 决策结果 :
- 数据将发往
TDM-LPAIF_RXTX-RX-PRIMARY(这是调音台的最终插口)。 - 采样率必须是 48000Hz ,位深 32bit。
snd_device_name为speaker(这告诉调音师去mixer_paths.xml里找名为speaker的开关序列)。
- 数据将发往
Step 4: 处理算法干扰策略 (<in_streams>)
如果此时用户突然开启了语音助理(即开启了录音流):
- XML 检查 :RM 查看
<in_streams>下PAL_STREAM_DEEP_BUFFER的ec_ref策略。 - 决策 :由于该流未被列入
disabled_stream,系统会同意将这首音乐的声音作为回声消除的参考信号,确保语音助理能"听清"指令而不受音乐干扰。
四、 标签与数据结构快速参考表
| XML 标签 | C++ 数据结构 | 对应变量 | 决策影响 |
|---|---|---|---|
<config_params> |
各类 bool/int | ResourceManager 成员 |
全局调度策略 (DSP Mix 等) |
<bt_codecs> |
std::map |
btCodecMap |
蓝牙编码链路加载 |
<controls> |
struct control_t |
ControlInfo |
调节插件的分发 |
<config_voice> |
struct vsid_info |
vsidInfo |
通话模式与 ID 切换 |
<in-device> |
struct deviceIn |
deviceInfo |
麦克风端口与算法注入 |
<out-device> |
struct deviceCap |
devInfo |
扬声器属性与保护策略 |
<in_streams> |
struct tx_ecinfo |
txEcInfo |
AEC 算法的回声参考过滤 |
五、 运行时决策流:以媒体播放 (Deep Buffer) 为例
当 Android 上层(如网易云音乐)请求播放音频时,系统会经历从 Audio HAL 到 PAL 再到 ResourceManager 的层层传递。
1. 上层调用链 (Upper Layer Usage)
- Framework :
AudioTrack.play()发起请求。 - Audio HAL (HIDL): Android 自动驾驶平台(SA8295P)的 Audio HAL 接收到请求。
- PAL API : HAL 调用
PalApi.h中的pal_stream_open。- 关键参数 :
pal_stream_type_t type = PAL_STREAM_DEEP_BUFFER。
- 关键参数 :
2. PAL 内部流创建 (Stream Creation)
在 Pal.cpp 中,系统会根据 type 创建对应的流对象:
pal_stream_open 是 PAL (Platform Audio Layer) 框架提供给上层应用(如 Audio HAL)的核心 API 接口,用于根据指定的属性和设备创建一个音频流句柄。
cpp
// android/android/vendor/qcom/opensource/pal/Pal.cpp
/**
* @brief 打开一个新的音频流
*
* @param attributes 流属性(如采样率、声道、用途等)
* @param no_of_devices 关联的设备数量
* @param devices 设备属性数组(如扬声器、耳机等)
* @param no_of_modifiers 修饰符数量
* @param modifiers 键值对形式的修饰符(用于特定算法配置)
* @param cb 用户注册的回调函数(用于接收异步事件)
* @param cookie 回调时透传的用户私有数据
* @param stream_handle [输出] 成功后返回的流句柄
* @return int32_t 成功返回 0,失败返回错误码
*/
int32_t pal_stream_open(struct pal_stream_attributes *attributes,
uint32_t no_of_devices, struct pal_device *devices,
uint32_t no_of_modifiers, struct modifier_kv *modifiers,
pal_stream_callback cb, uint64_t cookie,
pal_stream_handle_t **stream_handle)
{
uint64_t *stream = NULL; // 用于存放转换后的句柄指针
Stream *s = NULL; // PAL 内部的 Stream 基类指针
int status;
std::shared_ptr<ResourceManager> rm = NULL;
// 获取资源管理器的单例实例,RM 负责管理所有音频资源和状态
rm = ResourceManager::getInstance();
if (!rm) {
PAL_ERR(LOG_TAG, "Invalid resource manager");
status = -EINVAL;
return status;
}
// 参数合法性检查:必须提供流属性
if (!attributes) {
status = -EINVAL;
PAL_ERR(LOG_TAG, "Invalid input parameters status %d", status);
return status;
}
PAL_INFO(LOG_TAG, "Enter, stream type:%d", attributes->type);
try {
// 使用工厂模式创建 Stream 对象。根据 attributes->type 的不同,
// 实际上会创建 StreamPCM, StreamCompress, StreamInCall 等子类对象。
s = Stream::create(attributes, devices, no_of_devices, modifiers,
no_of_modifiers);
} catch (const std::exception& e) {
// 捕获创建过程中的异常,并执行特定的异常处理(如通知回调)
status = -EINVAL;
PAL_ERR(LOG_TAG, "Stream create failed: %s", e.what());
Stream::handleStreamException(attributes, cb, cookie);
goto exit;
}
if (!s) {
status = -EINVAL;
PAL_ERR(LOG_TAG, "stream creation failed status %d", status);
goto exit;
}
// 执行流的打开操作。此步骤会根据配置选择合适的硬件后端、配置 Mixer 控件、
// 分配 DSP 资源(Graph)等。
status = s->open();
if (0 != status) {
PAL_ERR(LOG_TAG, "pal_stream_open failed with status %d", status);
// 如果打开失败,需要尝试关闭并销毁已分配的对象
if (s->close() != 0) {
PAL_ERR(LOG_TAG, "stream closed failed.");
}
delete s;
goto exit;
}
// 如果用户提供了回调函数,则将其注册到流中,用于后续的上报(如数据耗尽、错误、唤醒事件)
if (cb)
s->registerCallBack(cb, cookie);
// 在资源管理器中初始化该流的用户计数器,用于管理该流的引用计数
rm->initStreamUserCounter(s);
// 初始化流内部使用的信号量,确保多线程操作(如同时 Start/Stop)的同步安全性
s->initStreamSmph();
// 将内部 Stream 对象指针强制转换为外部可见的流句柄
stream = reinterpret_cast<uint64_t *>(s);
*stream_handle = stream;
exit:
// 打印退出日志,显示生成的句柄地址和状态
PAL_INFO(LOG_TAG, "Exit. Value of stream_handle %pK, status %d", stream, status);
return status;
}
关键逻辑总结:
- 面向对象多态性:通过 Stream::create 实现了多态。根据传入的 pal_stream_type,系统会自动路由到对应的业务逻辑(例如 StreamPCM
处理普通音频,StreamSoundTrigger 处理语音唤醒)。 - 两阶段构造:代码采用了典型的"构造 + 显式 open"模式。Stream::create 只负责对象的内存分配和基本成员初始化,而复杂的硬件配置逻辑全部封装在 s->open() 中。
- 资源抽象:该 API 隐藏了底层声卡、DSP 节点(Graph)和 ALSA Mixer 的复杂性。调用者只需要描述"我要什么样的流"和"在哪个设备上放",PAL 就会通过
ResourceManager 查找之前 XML 解析出来的配置并自动完成链路搭建。 - 句柄封装:将 C++ 对象指针 Stream* 转换为 uint64_t* 作为句柄返回给 C 风格的接口层,这是典型的跨语言或跨模块接口设计方式。
Stream::create
Stream::create 是 PAL 框架中的静态工厂方法。它的核心作用是根据传入的流属性(sAttr),识别出具体的音频场景,并实例化对应的子类对象(如 StreamPCM、StreamCompress 等)。
c
/**
* @brief 静态工厂方法,用于创建具体的 Stream 子类实例
*
* @param sAttr 流属性(采样率、格式、类型等)
* @param dAttr 设备属性数组(关联的硬件设备)
* @param noOfDevices 设备数量
* @param modifiers 修饰符(用于算法配置)
* @param noOfModifiers 修饰符数量
* @return Stream* 返回创建的 Stream 子类指针
*/
Stream* Stream::create(struct pal_stream_attributes *sAttr, struct pal_device *dAttr,
uint32_t noOfDevices, struct modifier_kv *modifiers, uint32_t noOfModifiers)
{
// 使用互斥锁保护流的创建过程,防止多线程竞争导致资源管理混乱
std::lock_guard<std::mutex> lock(mBaseStreamMutex);
Stream* stream = NULL;
int status = 0;
uint32_t count = 0;
struct pal_device *palDevsAttr = nullptr;
std::vector <Stream *> streamsToSwitch;
struct pal_device streamDevAttr;
PAL_VERBOSE(LOG_TAG, "Enter.");
// 参数校验:流属性必须存在;如果指定了设备数量,设备属性数组也不能为空
if (!sAttr || ((noOfDevices > 0) && !dAttr)) {
PAL_ERR(LOG_TAG, "Invalid input paramters");
goto exit;
}
/* 获取资源管理器 (ResourceManager) 单例 */
if (!rm) {
rm = ResourceManager::getInstance();
if (!rm) {
PAL_ERR(LOG_TAG, "ResourceManager getInstance failed");
goto exit;
}
}
PAL_VERBOSE(LOG_TAG,"get RM instance success and noOfDevices %d \n", noOfDevices);
// 特殊流类型处理:非隧道流或上下文代理流不需要复杂的设备配置,直接跳转到创建逻辑
if (sAttr->type == PAL_STREAM_NON_TUNNEL || sAttr->type == PAL_STREAM_CONTEXT_PROXY)
goto stream_create;
// 为内部使用的设备属性数组分配内存
palDevsAttr = (pal_device *)calloc(noOfDevices, sizeof(struct pal_device));
if (!palDevsAttr) {
PAL_ERR(LOG_TAG, "palDevsAttr not created");
goto exit;
}
// 通话中播放音乐流也不需要在此处遍历配置设备
if (sAttr->type == PAL_STREAM_VOICE_CALL_MUSIC)
goto stream_create;
// --- 核心循环:准备并验证每个设备的配置 ---
for (int i = 0; i < noOfDevices; i++) {
struct pal_device_info devinfo = {};
palDevsAttr[i] = {};
// 场景:超声波 (Ultrasound) 流
// 自动分配输入和输出设备 ID(通常第一个是输出,第二个是输入)
if (sAttr->type == PAL_STREAM_ULTRASOUND) {
if (i == 0) { // 第一个分配为输出设备
if (rm->IsDedicatedBEForUPDEnabled())
dAttr[i].id = PAL_DEVICE_OUT_ULTRASOUND;
else
dAttr[i].id = PAL_DEVICE_OUT_HANDSET;
} else { // 第二个分配为输入设备
dAttr[i].id = PAL_DEVICE_IN_ULTRASOUND_MIC;
}
}
// 场景:车载电话总线流,如果蓝牙 SCO2 已连接,则强制切换到蓝牙输出
if (sAttr->type == PAL_STREAM_PLAYBACK_BUS && strcmp(sAttr->bus_addr, "BUS03_PHONE") == 0) {
if (rm->isDeviceReady(PAL_DEVICE_OUT_BLUETOOTH_SCO2)) {
PAL_INFO(LOG_TAG, "BT SCO2 is ready connected, set output device to SCO2\n");
dAttr[i].id = PAL_DEVICE_OUT_BLUETOOTH_SCO2;
}
}
// 拷贝设备 ID 和地址(尤其是 USB 设备需要物理地址)
palDevsAttr[count].id = dAttr[i].id;
if (palDevsAttr[count].id == PAL_DEVICE_OUT_USB_DEVICE ||
palDevsAttr[count].id == PAL_DEVICE_OUT_USB_HEADSET ||
palDevsAttr[count].id == PAL_DEVICE_IN_USB_DEVICE ||
palDevsAttr[count].id == PAL_DEVICE_IN_USB_HEADSET) {
palDevsAttr[count].address = dAttr[i].address;
}
// 拷贝自定义键值 (Custom Key),用于从 XML 中匹配特定的后端配置
if (strlen(dAttr[i].custom_config.custom_key)) {
strlcpy(palDevsAttr[count].custom_config.custom_key, dAttr[i].custom_config.custom_key, PAL_MAX_CUSTOM_KEY_SIZE);
}
// 从 ResourceManager 中获取设备的最终配置(结合了 XML 中的默认值和当前流的要求)
if (palDevsAttr[count].address.card_id != DUMMY_SND_CARD) {
status = rm->getDeviceConfig((struct pal_device *)&palDevsAttr[count], sAttr); // <------- 重点关注这里
if (status) {
PAL_ERR(LOG_TAG, "Not able to get Device config %d", status);
goto exit;
}
// 检查并更新分组设备(Group Device)配置,确保多个设备协同工作时配置正确
status = rm->checkAndUpdateGroupDevConfig((struct pal_device *)&palDevsAttr[count], sAttr,
streamsToSwitch, &streamDevAttr, true);
}
count++;
}
stream_create:
// --- 子类实例化:根据流类型选择具体的实现类 ---
if (rm->isStreamSupported(sAttr, palDevsAttr, noOfDevices)) {
switch (sAttr->type) {
// 大多数 PCM 场景(低延迟、深度缓冲、通话、总线播放等)使用 StreamPCM
case PAL_STREAM_LOW_LATENCY:
case PAL_STREAM_DEEP_BUFFER:
case PAL_STREAM_VOIP_TX:
case PAL_STREAM_VOICE_CALL:
case PAL_STREAM_PLAYBACK_BUS:
// ... (省略其他 PCM 类型)
stream = new StreamPCM(sAttr, palDevsAttr, noOfDevices, modifiers, noOfModifiers, rm);
break;
// 压缩音频播放(如 MP3 Offload)使用 StreamCompress
case PAL_STREAM_COMPRESSED:
stream = new StreamCompress(sAttr, palDevsAttr, noOfDevices, modifiers, noOfModifiers, rm);
break;
// 语音唤醒 (Voice UI) 使用 StreamSoundTrigger
case PAL_STREAM_VOICE_UI:
stream = new StreamSoundTrigger(sAttr, palDevsAttr, noOfDevices, modifiers, noOfModifiers, rm);
break;
// 通话录音或通话中背景音乐使用 StreamInCall
case PAL_STREAM_VOICE_CALL_RECORD:
case PAL_STREAM_VOICE_CALL_MUSIC:
stream = new StreamInCall(sAttr, palDevsAttr, noOfDevices, modifiers, noOfModifiers, rm);
break;
// 自动化内容检测使用 StreamACD
case PAL_STREAM_ACD:
stream = new StreamACD(sAttr, palDevsAttr, noOfDevices, modifiers, noOfModifiers, rm);
break;
// 超声波流使用 StreamUltraSound
case PAL_STREAM_ULTRASOUND:
stream = new StreamUltraSound(sAttr, palDevsAttr, noOfDevices, modifiers, noOfModifiers, rm);
break;
default:
PAL_ERR(LOG_TAG, "unsupported stream type 0x%x", sAttr->type);
break;
}
} else {
PAL_ERR(LOG_TAG,"Requested config not supported");
goto exit;
}
exit:
// 清理临时分配的设备属性内存
if (palDevsAttr) {
free(palDevsAttr);
}
return stream; // 返回创建好的子类实例
}
关键点总结:
- 策略模式与工厂模式结合:Stream::create 充当决策中心。它不负责具体的音频链路搭建,而是通过 switch-case 决定由哪个具体的子类来负责。
- 配置注入:在创建对象之前,它先通过 rm->getDeviceConfig 将 XML 中解析到的硬件参数(如采样率、位深、后端名称)注入到设备属性中。
- 设备预处理:针对 Ultrasound 等特殊场景,它在创建流之前会强制修正设备 ID,保证硬件路径符合预期。
- 资源管理器的深度协作:该方法高度依赖 ResourceManager 来判断某种配置是否受支持(isStreamSupported),这体现了 PAL 架构中配置驱动的设计理念。
StreamPCM::StreamPCM
StreamPCM 是 PAL 框架中处理最广泛的音频流类型(如音乐播放、通话、录音等)的类。其构造函数负责初始化 PCM 流的核心属性、分配内存、创建关联的 Session 和 Device
对象
c
/**
* @brief StreamPCM 构造函数,初始化 PCM 流对象
*
* @param sattr 流属性(采样率、位深、通道数等)
* @param dattr 关联的设备属性数组
* @param no_of_devices 设备数量
* @param modifiers 修饰符(暂未使用)
* @param no_of_modifiers 修饰符数量
* @param rm 资源管理器智能指针
*/
StreamPCM::StreamPCM(const struct pal_stream_attributes *sattr, struct pal_device *dattr,
const uint32_t no_of_devices, const struct modifier_kv *modifiers,
const uint32_t no_of_modifiers, const std::shared_ptr<ResourceManager> rm)
{
// 加锁保护构造过程,防止多线程竞争(虽然构造函数通常只在单线程中调用,但为了安全起见)
mStreamMutex.lock();
uint32_t in_channels = 0, out_channels = 0;
uint32_t attribute_size = 0;
// 检查声卡状态:如果声卡离线(如下线重启中),则无法创建流,直接抛出异常
if (rm->cardState == CARD_STATUS_OFFLINE) {
PAL_ERR(LOG_TAG, "Sound card offline, can not create stream");
usleep(SSR_RECOVERY); // 等待一段时间让系统恢复
mStreamMutex.unlock();
throw std::runtime_error("Sound card offline");
}
// 初始化成员变量
session = NULL;
mGainLevel = -1;
std::shared_ptr<Device> dev = nullptr;
mStreamAttr = (struct pal_stream_attributes *)nullptr;
inBufSize = BUF_SIZE_CAPTURE; // 设置默认录音缓冲区大小
outBufSize = BUF_SIZE_PLAYBACK; // 设置默认播放缓冲区大小
inBufCount = NO_OF_BUF;
outBufCount = NO_OF_BUF;
mDevices.clear();
mPalDevice.clear();
currentState = STREAM_IDLE; // 初始状态为空闲
cachedState = STREAM_IDLE;
bool isDeviceConfigUpdated = false;
uint32_t input_instance_id = 0;
PAL_VERBOSE(LOG_TAG, "Enter");
// 暂不处理修饰符 (Modifiers),预留接口
mNoOfModifiers = 0;
mModifiers = (struct modifier_kv *) (NULL);
std::ignore = modifiers;
std::ignore = no_of_modifiers;
// 分配并初始化音量数据结构,默认音量设为 1.0 (最大)
mVolumeData = (struct pal_volume_data *)malloc(sizeof(struct pal_volume_data)
+sizeof(struct pal_channel_vol_kv));
if (!mVolumeData) {
PAL_ERR(LOG_TAG, "Failed to allocate memory for volume data");
mStreamMutex.unlock();
throw std::runtime_error("failed to allocate memory for volume data");
}
mVolumeData->no_of_volpair = 1;
mVolumeData->volume_pair[0].channel_mask = 0x03; // 立体声掩码
mVolumeData->volume_pair[0].vol = 1.0f;
// 参数校验:流属性和设备属性必须有效
if (!sattr || !dattr) {
PAL_ERR(LOG_TAG,"invalid arguments");
mStreamMutex.unlock();
throw std::runtime_error("invalid arguments");
}
// 为流属性结构体分配内存并深拷贝传入的属性
attribute_size = sizeof(struct pal_stream_attributes);
mStreamAttr = (struct pal_stream_attributes *) calloc(1, attribute_size);
if (!mStreamAttr) {
PAL_ERR(LOG_TAG, "malloc for stream attributes failed %s", strerror(errno));
mStreamMutex.unlock();
throw std::runtime_error("failed to malloc for stream attributes");
}
ar_mem_cpy(mStreamAttr, sizeof(pal_stream_attributes), sattr, sizeof(pal_stream_attributes));
// 校验并限制通道数,防止越界
if (mStreamAttr->in_media_config.ch_info.channels > PAL_MAX_CHANNELS_SUPPORTED) {
mStreamAttr->in_media_config.ch_info.channels = PAL_MAX_CHANNELS_SUPPORTED;
}
if (mStreamAttr->out_media_config.ch_info.channels > PAL_MAX_CHANNELS_SUPPORTED) {
mStreamAttr->out_media_config.ch_info.channels = PAL_MAX_CHANNELS_SUPPORTED;
}
// 特殊逻辑:对于输入流,如果没有找到总线地址,则默认实例 ID 为 0
if (mStreamAttr->direction == PAL_AUDIO_INPUT && mStreamAttr->bus_addr) {
input_instance_id = 0;
}
// --- 核心步骤 1: 创建 Session 对象 ---
// Session 负责管理音频数据流的生命周期和状态转换
PAL_VERBOSE(LOG_TAG, "Create new Session");
session = Session::makeSession(rm, sattr);
if (!session) {
PAL_ERR(LOG_TAG, "session creation failed");
free(mStreamAttr);
mStreamMutex.unlock();
throw std::runtime_error("failed to create session object");
}
// --- 核心步骤 2: 创建并关联 Device 对象 ---
PAL_VERBOSE(LOG_TAG, "Create new Devices with no_of_devices - %d", no_of_devices);
for (int i = 0; i < no_of_devices; i++) {
// 通过 Device::getInstance 获取设备实例(如果是单例设备则返回现有实例,否则新建)
dev = Device::getInstance((struct pal_device *)&dattr[i] , rm);
if (!dev) {
PAL_ERR(LOG_TAG, "Device creation failed");
free(mStreamAttr);
mStreamMutex.unlock();
throw std::runtime_error("failed to create device object");
}
// 尝试更新设备的配置以匹配流的需求(例如,如果流是 24-bit,则尝试将设备配置为 24-bit)
// 注意:输入设备通常需要支持并发录音,因此不轻易更新配置以免影响其他流
if (mStreamAttr->direction != PAL_AUDIO_INPUT) {
mStreamMutex.unlock();
isDeviceConfigUpdated = rm->updateDeviceConfig(&dev, &dattr[i], sattr);
mStreamMutex.lock();
}
// 将关联好的 Device 对象和原始属性保存到成员变量中
mDevices.push_back(dev);
mPalDevice.push_back(dattr[i]);
dev = nullptr;
}
// 注册软暂停回调(仅针对输出流),用于处理如 Audio HAL 触发的暂停
if (mStreamAttr->direction == PAL_AUDIO_OUTPUT )
session->registerCallBack(handleSoftPauseCallBack, (uint64_t)this);
mStreamMutex.unlock();
// --- 核心步骤 3: 注册流到资源管理器 ---
// 必须在解锁后调用,因为 rm->registerStream 内部可能会获取 rm 的锁,避免死锁
rm->registerStream(this);
PAL_VERBOSE(LOG_TAG, "Exit. state %d", currentState);
return;
}
关键逻辑总结:
- 资源校验:在创建流之前,首先检查声卡状态(CARD_STATUS_OFFLINE),防止在硬件不可用时进行无谓的操作。
- 对象组合:StreamPCM 并不直接操作硬件,而是通过组合 Session(负责控制流逻辑)和 Device(负责硬件配置)来实现功能。这是一种典型的桥接模式或组合模式。
- 配置协商:在关联设备时,会调用
rm->updateDeviceConfig。这一步非常关键,它确保了如果一个新的流(例如高解析度音乐)需要更高的采样率或位深,底层设备会被重新配置以满足需求,除非该设备已经
被其他流锁定。 - 并发安全:构造函数中使用了互斥锁 mStreamMutex,并且在调用 rm->registerStream 前特意解锁,这是为了避免与 ResourceManager 中的锁发生死锁(Lock Ordering
Inversion)。
StreamPCM::open
StreamPCM::open 是音频流生命周期中的关键步骤。在 Stream::create 完成对象创建和属性验证后,调用 open 来正式启动底层资源,包括初始化 DSP
会话(Session)和打开关联的硬件设备(Device)。
c
/**
* @brief 打开 PCM 流,初始化 Session 和 Device,并应用特定的音效配置
*
* @return int32_t 成功返回 0,失败返回错误码
*/
int32_t StreamPCM::open()
{
int32_t status = 0;
int32_t ret = 0;
PAL_DBG(LOG_TAG, "Enter. session handle - %pK device count - %zu", session,
mDevices.size());
// 1. 加锁保护,确保 open 操作的原子性
mStreamMutex.lock();
// 2. 检查声卡状态:如果声卡处于离线状态(如 SSR 过程中),直接返回错误
if (rm->cardState == CARD_STATUS_OFFLINE) {
PAL_ERR(LOG_TAG, "Sound card offline, can not open stream");
usleep(SSR_RECOVERY);
status = -EIO; // 返回 I/O 错误
goto exit;
}
// 3. 状态机检查:只有处于 STREAM_IDLE(空闲)状态的流才能执行 open
if (currentState == STREAM_IDLE) {
// --- 核心步骤 A: 打开 Session ---
// session->open 会根据流属性在 DSP 中搭建对应的处理拓扑(Graph)
status = session->open(this);
if (0 != status) {
PAL_ERR(LOG_TAG, "session open failed with status %d", status);
goto exit;
}
PAL_VERBOSE(LOG_TAG, "session open successful");
// --- 核心步骤 B: 特殊功能预检测 (双单声道 Dual Mono) ---
bool checkDeviceCustomKeyForDualMono = false;
// 检查资源管理器是否全局开启了双单声道功能
if (rm->isDualMonoEnabled == true) {
// 场景:通常仅对低延迟 (Low Latency) 流开启此功能
if (mStreamAttr->type == PAL_STREAM_LOW_LATENCY) {
checkDeviceCustomKeyForDualMono = true;
}
}
// --- 核心步骤 C: 打开所有关联的硬件设备 ---
for (int32_t i = 0; i < mDevices.size(); i++) {
// 调用每个设备的 open 方法(如打开 ALSA PCM 节点、下发后端配置)
status = mDevices[i]->open();
if (0 != status) {
PAL_ERR(LOG_TAG, "device open failed with status %d", status);
goto exit;
}
// --- 核心步骤 D: 应用特定设备的音效参数 (以 Dual Mono 为例) ---
if (checkDeviceCustomKeyForDualMono) {
struct pal_device deviceAttribute;
// 获取设备的最终属性
ret = mDevices[i]->getDeviceAttributes(&deviceAttribute);
if (ret) {
PAL_ERR(LOG_TAG, "getDeviceAttributes failed with status %d", ret);
}
// 场景:如果当前设备是扬声器且配置了 "speaker-safe" 模式
if (deviceAttribute.id == PAL_DEVICE_OUT_SPEAKER &&
!strncmp(deviceAttribute.custom_config.custom_key,
"speaker-safe", sizeof("speaker-safe"))) {
uint8_t* paramData = NULL;
// 构建 Dual Mono 的参数负载
ret = PayloadBuilder::payloadDualMono(¶mData);
if (ret) {
PAL_ERR(LOG_TAG, "failed to create dual mono info");
continue;
}
// 将参数下发给 DSP 模块 (PAL_PARAM_ID_UIEFFECT)
// 作用:通常用于在扬声器保护模式下,将左右声道合并处理或应用特定增益
ret = session->setParameters(this, PER_STREAM_PER_DEVICE_MFC,
PAL_PARAM_ID_UIEFFECT, paramData);
if (ret) {
PAL_ERR(LOG_TAG, "failed to set dual mono param.");
}
free(paramData); // 释放临时负载内存
}
}
}
// 4. 更新状态机:流已进入初始化完成状态,可以进行 start/write 等操作
currentState = STREAM_INIT;
PAL_DBG(LOG_TAG, "streamLL opened. state %d", currentState);
} else if (currentState == STREAM_INIT) {
// 如果流已经处于 INIT 状态,说明已经打开过,直接返回成功
PAL_INFO(LOG_TAG, "Stream is already opened, state %d", currentState);
status = 0;
goto exit;
} else {
// 状态异常处理
PAL_ERR(LOG_TAG, "Stream is not in correct state %d", currentState);
status = -EINVAL;
goto exit;
}
exit:
// 解锁并退出
mStreamMutex.unlock();
PAL_VERBOSE(LOG_TAG, "Exit ret %d", status)
return status;
}
关键逻辑总结:
- 分层初始化:open 遵循了 Session (软件/DSP 拓扑) -> Device (硬件/后端端口) 的初始化顺序。只有 Session 打开成功,才能确信 DSP
中已经存在处理节点,从而进一步配置硬件。 - 状态机保护:函数通过 currentState 严格控制流的转换过程。这防止了上层应用重复打开同一个流,或者在错误的生命周期阶段执行 open 操作。
- 动态音效下发:代码展示了如何利用 custom_key(如 speaker-safe)在运行时识别特殊场景。PayloadBuilder 和 session->setParameters 的结合是 PAL 动态控制 DSP
模块参数(如双单声道切换、MFC 均衡器设置等)的标准模式。 - SA8295P 车载场景相关:speaker-safe 模式在车载环境中非常常见。当系统检测到扬声器过热或处于某种安全限制状态时,通过切换到该 custom_key,open 函数会自动向
DSP 下发保护性参数,确保硬件安全。
3. ResourceManager 决策逻辑源码分析
StreamPCM 在初始化时会调用 rm->getDeviceConfig 来确定具体的硬件参数。
ResourceManager::getDeviceConfig
ResourceManager::getDeviceConfig 函数是 PAL 框架中用于确定硬件设备最终运行参数的核心逻辑。它结合了 XML 静态配置、流的请求参数以及特定硬件(如
USB、蓝牙、HDMI)的动态能力,协商出最合适的采样率、位深和声道配置。
c
/**
* @brief 根据流属性和 XML 配置,获取并协商设备的最终配置参数
*
* @param deviceattr [输入/输出] 传入设备 ID,输出协商后的配置(采样率、位深等)
* @param sAttr [输入] 关联的流属性(请求的参数)
* @return int32_t 成功返回 0
*/
int32_t ResourceManager::getDeviceConfig(struct pal_device *deviceattr,
struct pal_stream_attributes *sAttr)
{
int32_t status = 0;
struct pal_channel_info dev_ch_info;
bool is_wfd_in_progress = false;
struct pal_stream_attributes tx_attr;
struct pal_device_info devinfo = {}; // 用于存放从 XML 中检索到的信息
// 基础参数校验
if (!deviceattr) {
PAL_ERR(LOG_TAG, "Invalid deviceattr");
return -EINVAL;
}
// --- 第一阶段:从 XML 配置中检索基础属性 ---
if (sAttr != NULL)
// 根据设备 ID、流类型和自定义 Key,从之前解析好的 XML 数据库中获取配置
getDeviceInfo(deviceattr->id, sAttr->type,
deviceattr->custom_config.custom_key, &devinfo); // <---- 这里很重要
else
// 如果流属性为空(通常用于设备初始化或查询),使用默认流类型 0
getDeviceInfo(deviceattr->id, (pal_stream_type_t)0,
deviceattr->custom_config.custom_key, &devinfo);
// --- 第二阶段:设置声道配置 ---
if (devinfo.channels == 0 || devinfo.channels > devinfo.max_channels) {
// 如果 XML 配置的声道数非法(为0或超过硬件最大限制),报错退出
PAL_ERR(LOG_TAG, "Invalid num channels[%d], max channels[%d]...", devinfo.channels, devinfo.max_channels);
status = -EINVAL;
goto exit;
}
dev_ch_info.channels = devinfo.channels;
// 获取声道映射关系(如 L/R 对应哪个插槽)
getChannelMap(&(dev_ch_info.ch_map[0]), devinfo.channels);
deviceattr->config.ch_info = dev_ch_info;
// --- 第三阶段:设置采样率 ---
if (devinfo.samplerate) {
// 如果 XML 中明确指定了采样率(Overwrite 模式),则强制使用
deviceattr->config.sample_rate = devinfo.samplerate;
} else {
// 否则:若流属性为空默认 48K;若不为空则尝试匹配流请求的采样率
deviceattr->config.sample_rate = ((sAttr == NULL) ? SAMPLINGRATE_48K :
(sAttr->direction == PAL_AUDIO_INPUT) ? sAttr->in_media_config.sample_rate : sAttr->out_media_config.sample_rate);
}
// --- 第四阶段:设置位深与音频格式 ---
if (devinfo.bit_width) {
// 优先使用 XML 中指定的位深
deviceattr->config.bit_width = devinfo.bit_width;
} else {
// 否则跟随流的请求位深(默认 16-bit)
deviceattr->config.bit_width = ((sAttr == NULL) ? BITWIDTH_16 :
(sAttr->direction == PAL_AUDIO_INPUT) ? sAttr->in_media_config.bit_width : sAttr->out_media_config.bit_width);
}
// 将位深(16/24/32)转换为 PAL 内部的格式 ID (如 PAL_AUDIO_FMT_PCM_S16_LE)
deviceattr->config.aud_fmt_id = bitWidthToFormat.at(deviceattr->config.bit_width);
// 特殊情况:如果 XML 中指定了具体的 bitFormatSupported(如 S24_3LE)
if (devinfo.bitFormatSupported != PAL_AUDIO_FMT_DEFAULT_PCM) {
deviceattr->config.aud_fmt_id = devinfo.bitFormatSupported;
deviceattr->config.bit_width = palFormatToBitwidthLookup(devinfo.bitFormatSupported);
}
// --- 第五阶段:特定硬件类型的动态协商 (针对热插拔或复杂设备) ---
switch (deviceattr->id) {
// 1. 有线耳机/麦克风:调用硬件特定类检查实际插拔的硬件支持能力
case PAL_DEVICE_IN_WIRED_HEADSET:
status = (HeadsetMic::checkAndUpdateSampleRate(&deviceattr->config.sample_rate));
break;
case PAL_DEVICE_OUT_WIRED_HEADPHONE:
case PAL_DEVICE_OUT_WIRED_HEADSET:
status = (Headphone::checkAndUpdateBitWidth(&deviceattr->config.bit_width) |
Headphone::checkAndUpdateSampleRate(&deviceattr->config.sample_rate));
break;
// 2. 蓝牙 A2DP:强制使用压缩格式(由蓝牙协议栈处理)
case PAL_DEVICE_OUT_BLUETOOTH_A2DP:
case PAL_DEVICE_IN_BLUETOOTH_A2DP:
deviceattr->config.aud_fmt_id = PAL_AUDIO_FMT_DEFAULT_COMPRESSED;
break;
// 3. 蓝牙 SCO (通话):根据当前 SCO 链路是宽带还是窄带更新采样率(8k/16k)
case PAL_DEVICE_OUT_BLUETOOTH_SCO:
case PAL_DEVICE_IN_BLUETOOTH_SCO_HEADSET:
{
std::shared_ptr<BtSco> scoDev = std::dynamic_pointer_cast<BtSco>(BtSco::getInstance(deviceattr, rm));
if (scoDev) scoDev->updateSampleRate(&deviceattr->config.sample_rate);
}
break;
// 4. USB 设备:这是最复杂的协商逻辑。USB 设备必须查询其描述符以获得最佳匹配
case PAL_DEVICE_OUT_USB_DEVICE:
case PAL_DEVICE_OUT_USB_HEADSET:
case PAL_DEVICE_IN_USB_DEVICE:
case PAL_DEVICE_IN_USB_HEADSET:
{
std::shared_ptr<USB> USB_dev = std::dynamic_pointer_cast<USB>(USB::getInstance(deviceattr, rm));
// selectBestConfig 会遍历 USB 设备支持的所有 Profile,选出一个最接近 sAttr 请求的
status = USB_dev->selectBestConfig(deviceattr, sAttr, (deviceattr->id < PAL_DEVICE_IN_USB_DEVICE), &devinfo);
// 针对 24-bit 处理对齐方式(LE 与 3LE)
if (deviceattr->config.bit_width == BITWIDTH_24) {
deviceattr->config.aud_fmt_id = (devinfo.bitFormatSupported == PAL_AUDIO_FMT_PCM_S24_LE) ?
PAL_AUDIO_FMT_PCM_S24_LE : PAL_AUDIO_FMT_PCM_S24_3LE;
}
}
break;
// 5. Proxy 设备 (用于跨核或跨进程传输,如 WFD 无线显示)
case PAL_DEVICE_IN_PROXY:
{
// 如果是 WFD 场景,尝试与输出端 Proxy 同步参数,确保链路一致性
struct pal_media_config *candidateConfig = &sAttr->in_media_config;
// ... (此处包含检查输出代理状态并同步采样率/声道的逻辑)
}
break;
// 6. HDMI / DisplayPort:根据 HDMI Sink 的 EDID 信息协商声道和采样率
case PAL_DEVICE_OUT_HDMI:
{
std::shared_ptr<DisplayPort> dp_device = std::dynamic_pointer_cast<DisplayPort>(DisplayPort::getInstance(deviceattr, rm));
int channels = dp_device->getMaxChannel();
// 协商声道:取流请求和硬件支持的最小值
if (channels > sAttr->out_media_config.ch_info.channels)
channels = sAttr->out_media_config.ch_info.channels;
// HDMI 规范要求:1声道必须转为2声道
if (channels == 1) channels = 2;
dev_ch_info.channels = channels;
deviceattr->config.ch_info = dev_ch_info;
// 查询硬件支持的最高采样率
if (!dp_device->isSupportedSR(NULL, sAttr->out_media_config.sample_rate))
deviceattr->config.sample_rate = dp_device->getHighestSupportedSR();
}
break;
default:
break;
}
exit:
// 打印最终确定的设备配置参数,用于问题排查
PAL_DBG(LOG_TAG, "device id 0x%x channels %d samplerate %d, bitwidth %d format %d SndDev %s",
deviceattr->id, deviceattr->config.ch_info.channels, deviceattr->config.sample_rate,
deviceattr->config.bit_width, deviceattr->config.aud_fmt_id, devinfo.sndDevName.c_str());
return status;
}
关键逻辑总结:
- 静态与动态结合:基础参数由 getDeviceInfo(XML 配置)决定,体现了"配置驱动"的思想;特殊硬件(USB, HDMI,
BT)则通过对应的类进行动态查询,体现了"硬件自适应"。 - 向下兼容与降级策略:在 HDMI 或 USB 协商中,如果硬件不支持流请求的高参数(如 192kHz),函数会自动查找"最高支持参数"进行降级,确保音频能正常响。
- 多子系统协作:该函数展现了 ResourceManager 如何调度 BtSco、USB、DisplayPort 等专用设备管理类来完成复杂的链路匹配。
- SA8295P 场景相关:在 HQX 车机平台上,PAL_DEVICE_OUT_PROXY 和 PAL_STREAM_PLAYBACK_BUS
是非常常见的,前者用于跨处理器的音频路由,后者用于车载总线音效的分发,此处的配置逻辑确保了这些虚拟/物理总线链路的参数一致性
ResourceManager::getDeviceInfo
此函数 ResourceManager::getDeviceInfo 是 PAL 资源管理器的核心逻辑之一,它实现了多级配置覆盖机制。其目的是根据"设备
ID"、"流类型(用例)"以及"自定义键值(Custom Key)"这三个维度,精确地从 XML 配置库中检索出最匹配的硬件参数(采样率、声道、位深等)。
cpp
// android/android/vendor/qcom/opensource/pal/resource_manager/src/ResourceManager.cpp
/**
* @brief 获取特定设备在特定用例和自定义键值下的配置信息
*
* @param deviceId 硬件设备 ID (如 PAL_DEVICE_OUT_SPEAKER)
* @param type 流类型/用例 (如 PAL_STREAM_LOW_LATENCY)
* @param key 自定义键值,用于区分同一设备在不同场景下的特殊配置 (如 "low-power")
* @param devinfo [输出] 用于存放获取到的最终设备属性
*/
void ResourceManager::getDeviceInfo(pal_device_id_t deviceId, pal_stream_type_t type, std::string key, struct pal_device_info *devinfo)
{
bool found = false;
// --- 第一级:匹配基础设备 (Device) ---
// 遍历从 resourcemanager.xml 中解析出的所有设备信息列表
for (int32_t i = 0; i < deviceInfo.size(); i++) {
if (deviceId == deviceInfo[i].deviceId) {
// 找到匹配的硬件设备,首先填入该设备的默认属性
devinfo->max_channels = deviceInfo[i].max_channel;
devinfo->channels = deviceInfo[i].channel;
devinfo->sndDevName = deviceInfo[i].sndDevName; // ALSA 设备名
devinfo->samplerate = deviceInfo[i].samplerate;
devinfo->isExternalECRefEnabledFlag = deviceInfo[i].isExternalECRefEnabled;
devinfo->priority = MIN_USECASE_PRIORITY; // 默认设置为最低优先级
devinfo->bit_width = deviceInfo[i].bit_width;
devinfo->bitFormatSupported = deviceInfo[i].bitFormatSupported;
// 初始化覆盖标志位为 false,表示目前使用的是基础默认值
devinfo->channels_overwrite = false;
devinfo->samplerate_overwrite = false;
devinfo->sndDevName_overwrite = false;
devinfo->bit_width_overwrite = false;
devinfo->fractionalSRSupported = deviceInfo[i].fractionalSRSupported;
// --- 第二级:匹配特定流用例 (Usecase) ---
// 遍历该设备下定义的所有用例覆盖配置
for (int32_t j = 0; j < deviceInfo[i].usecase.size(); j++) {
if (type == deviceInfo[i].usecase[j].type) {
// 如果该用例配置了特定的声道数,则覆盖默认值
if (deviceInfo[i].usecase[j].channel) {
devinfo->channels = deviceInfo[i].usecase[j].channel;
devinfo->channels_overwrite = true; // 标记已被用例覆盖
PAL_VERBOSE(LOG_TAG, "getting overwritten channels %d...", devinfo->channels);
}
// 如果该用例配置了特定的采样率,则覆盖
if (deviceInfo[i].usecase[j].samplerate) {
devinfo->samplerate = deviceInfo[i].usecase[j].samplerate;
devinfo->samplerate_overwrite = true;
}
// 如果该用例指定了特殊的后端 ALSA 设备名
if (!(deviceInfo[i].usecase[j].sndDevName).empty()) {
devinfo->sndDevName = deviceInfo[i].usecase[j].sndDevName;
devinfo->sndDevName_overwrite = true;
}
// 更新优先级
if (deviceInfo[i].usecase[j].priority) {
devinfo->priority = deviceInfo[i].usecase[j].priority;
}
// 更新位深
if (deviceInfo[i].usecase[j].bit_width) {
devinfo->bit_width = deviceInfo[i].usecase[j].bit_width;
devinfo->bit_width_overwrite = true;
}
// --- 第三级:匹配自定义配置 (Custom Config) ---
// 遍历该用例下所有的自定义键值配置段
for (int32_t k = 0; k < deviceInfo[i].usecase[j].config.size(); k++) {
// 比较传入的 key 与 XML 中的 custom-config 标签的 key 属性
if (!deviceInfo[i].usecase[j].config[k].key.compare(key)) {
// 如果 Key 匹配,执行最高优先级的覆盖
if (deviceInfo[i].usecase[j].config[k].channel) {
devinfo->channels = deviceInfo[i].usecase[j].config[k].channel;
devinfo->channels_overwrite = true;
}
if (deviceInfo[i].usecase[j].config[k].samplerate) {
devinfo->samplerate = deviceInfo[i].usecase[j].config[k].samplerate;
devinfo->samplerate_overwrite = true;
}
if (!(deviceInfo[i].usecase[j].config[k].sndDevName).empty()) {
devinfo->sndDevName = deviceInfo[i].usecase[j].config[k].sndDevName;
devinfo->sndDevName_overwrite = true;
}
if (deviceInfo[i].usecase[j].config[k].priority &&
deviceInfo[i].usecase[j].config[k].priority != MIN_USECASE_PRIORITY) {
devinfo->priority = deviceInfo[i].usecase[j].config[k].priority;
}
if (deviceInfo[i].usecase[j].config[k].bit_width) {
devinfo->bit_width = deviceInfo[i].usecase[j].config[k].bit_width;
devinfo->bit_width_overwrite = true;
}
found = true;
break; // 已找到最匹配的 Custom 配置,跳出内层循环
}
}
}
}
}
}
}
关键逻辑总结:
- 优先级体系:
- Level 1 (最低):
<in-device> / <out-device>的直接子项。 - Level 2 (中等): 设备内部
<usecase>标签定义的属性。 - Level 3 (最高): 用例内部
<custom-config>标签定义的属性。
- Level 1 (最低):
- 覆盖标志位:_overwrite 成员的存在是为了让调用者知道当前的采样率或声道数是"强制指定"的,还是使用的硬件"默认能力"。
- 应用场景:例如,同一个扬声器在普通音乐播放时(PAL_STREAM_DEEP_BUFFER)可能使用 48kHz,但在处理超高清音频(使用特殊的 Custom
Key)时,可以通过此函数检索出 192kHz 的配置,从而自动完成硬件参数的动态调整。
4. 决策时序图 (增强版)
DSP Hardware ResourceManager (Memory Cache) PAL (StreamPCM) Audio HAL (Primary) DSP Hardware ResourceManager (Memory Cache) PAL (StreamPCM) Audio HAL (Primary) 遍历 deviceInfo 向量 媒体音乐从 A2B 扬声器流出 pal_stream_open(DEEP_BUFFER) getDeviceConfig(SPEAKER, DEEP_BUFFER) 返回 {48k, 32bit, "speaker", Metadata: 0xAD...} getBackendName(SPEAKER) 返回 "TDM-LPAIF_RXTX-RX-PRIMARY" 1. 加载参数 (SetParam) 2. 注入算法 (Metadata) 3. 开启通路 (Start Stream)
总结 :resourcemanager.xml 的设计哲学是 "声明式驱动"**。它将复杂的 C++ 内部逻辑转化为可配置的标签,使得工程师可以通过修改文本,精准控制从蓝牙编码到麦克风降噪、再到硬件端口分配的每一个决策点。
5. 如何和底层 dsp 交互
通过上面的分析,我们不难看出 在使用 ResourceManager 决策逻辑时, 仅仅是通过 getDeviceConfig 填充了 pal_stream_attributes , 并没有看到, 如何拿着 pal_stream_attributes 去和底层 dsp 做交互。
在 PAL 架构中,Stream 是业务层的抽象,而 Session 才是真正的"外交官",负责与底层 DSP(通常通过 ALSA 或 AGM)打交道。
以下是 pal_stream_attributes 从内存结构变成 DSP 指令的完整演进过程:
- 核心链路:谁在拿这些属性?
在 StreamPCM::open() 中,你会看到这一行:
c
status = session->open(this); // 'this' 指向当前 Stream 对象,里面存着 sattr
这里的 session 实际上是 SessionAlsaPcm(在大多数 PCM 场景下)的实例。
- 交互第一步:路由转换(Mixer Controls)
DSP 需要知道音频流应该走哪条路(Routing)。SessionAlsaPcm 会向 RM 询问:"这个设备对应的 ALSA 控制键叫什么?"
- 源码逻辑: SessionAlsaPcm 会调用 rm->getBackendName() 获取你在 XML 里定义的 back_end_name(如 WSA_CODEC_DMA_RX_0)。
- 交互方式: 它使用 tinyalsa 的 mixer_ctl_set_enum_by_string 等接口,设置底层的 Audio Route。
- DSP 视角: 收到指令,打通从内部 Frontend(FE)到 Backend(BE)的通路。
- 核心交互:Payload 生成(数据协议转换)
这是 pal_stream_attributes 转换的关键。DSP 不认识 C++ 对象,它只认识特定的 二进制协议(Payload)。
- 组件: PayloadBuilder 类。
- 源码逻辑: 在 SessionAlsaPcm::open 或 prepare 阶段,会调用类似:
c
// 伪代码,展示逻辑
PayloadBuilder builder;
builder.payloadStreamConfig(&payload, &size, mStreamAttr);
- 转换内容: PayloadBuilder 会读取 sattr->out_media_config 中的采样率、位深、声道,将它们填入一个 DSP 模组(如 MODULE_ID_MFC - Media Format
Converter)能理解的数据包中。
- 交互第二步:下发参数(Set Parameters)
生成的二进制 Payload 需要通过"邮差"送给 DSP。
- 交互方式:
- 方法 A (通过 ALSA Control): 调用 pcm_set_config(TinyALSA)将基础的采样率/格式信息传给内核驱动。
- 方法 B (通过专用 IOCTL): 对于复杂的算法配置(如均衡器、增益),调用 mixer_ctl_set_array 将 Payload 发送到对应的 Mixer 节点。
- 代码体现:
c
// 在 SessionAlsaPcm 中常见
status = SessionAlsaPcm::setParameters(stream, ..., PAL_PARAM_ID_STREAM_CONFIG, payload);
- 交互第三步:开启硬件 DMA (PCM Open)
最后,Session 会打开 Linux 内核中的 PCM 设备文件。
- 代码体现:
c
// 调用 TinyALSA 接口
pcm = pcm_open(rm->getVirtualSndCard(), pcm_device_id, PCM_OUT, &config);
- 交互后果: 这个操作会触发驱动程序向 DSP 发送
GSL_OPEN指令。此时,DSP 正式根据之前下发的 sattr 属性,分配内部的内存 Buffer 和时钟。
梳理总结图:
| 阶段 | 执行者 | 操作 | 属性的去向 |
|---|---|---|---|
| Research | ResourceManager | getDeviceConfig | 填充 pal_device 的 config 结构体。 |
| Mapping | SessionAlsaPcm | rm->getBackEndName | 确定对应的 ALSA 设备名(如 WSA-STEREO)。 |
| Encoding | PayloadBuilder | payloadStreamConfig | [关键点] 将 sattr 翻译成二进制 Payload。 |
| Routing | Mixer | mixer_ctl_set | 告诉 DSP:把这个流导向刚才找到的 BE 端口。 |
| Configure | TinyALSA | pcm_open / ioctl | 将 Payload 下发到 DSP 内存,设置模块参数。 |
| Running | Kernel Driver | pcm_write | 数据流开始根据 sattr 定义的格式进行传输。 |
因为在高通的这套代码里,SessionAlsaPcm.cpp 或 SessionAlsaCompress.cpp 承载了这部分逻辑。ResourceManager 只负责"提供数据支持",真正的底层交互逻辑被封装在 |
|||
| Session 的实现类中,通过调用 TinyALSA 或 AGM (Audio Graph Manager) 的 API 完成了从 C++ 结构体 -> 内核 IOCTL -> DSP 控制协议 的跨越。 |
六、 工程师实战:如何配置资源管理器文件?
在 SA8295P 开发中,工程师通常需要根据具体的硬件拓扑(如 A2B 扬声器布局、麦克风阵列)修改 resourcemanager.xml。以下是核心配置项的通俗解读。
1. 全局"控制台":<config_params>
这是整个音频系统的"总开关"。
native_audio_mode:- 含义:音频混合模式。
- 配置建议 :通常设为
multiple_mix_dsp。这表示所有的音频混音都在 DSP 中完成,能显著降低 CPU 负载。
max_sessions:- 含义:最大并发"通话/播放"任务数。
- 配置建议 :车机系统复杂,建议设为
128。如果设得太小,当导航、警示音、媒体音乐同时响时,可能会有声音被"顶掉"。
logging_level:- 含义:PAL 层的日志等级。
- 配置建议 :开发阶段设为
3(DEBUG) 或4(VERBOSE),量产设为2(INFO)。
2. 蓝牙"翻译官":<bt_codecs>
定义蓝牙音频如何工作。
<codec>标签 :codec_format: 编码格式(如CODEC_TYPE_AAC,CODEC_TYPE_APTX)。codec_type: 角色(enc编码发给耳机,dec接收手机音频)。codec_library: 对应的算法库(.so文件)。- 配置建议:如果你需要支持新的高清编码(如 LDAC),必须在此处关联其动态库。
3. 功能"遥控器":<controls>
决定音量调节、延迟控制等插件如何分配给不同的音频流。
<control>: 定义一个控制项(如PLUGIN_CONTROL_VOLUME)。<plugin>: 指定处理逻辑的库。<plugin_usecase>:- 含义:过滤器。
- 配置建议 :只有在这里列出的流类型(如
PAL_STREAM_PLAYBACK_BUS)才会受此插件控制。如果你发现调节某个 App 的音量没反应,检查该 App 使用的流类型是否在此列表中。
4. 硬件"身份证":<device_profile>
这是最常修改的部分,定义了麦克风和扬声器的物理属性。
<in-device>/<out-device>: 输入(麦克风)或输出(扬声器)设备。<id>: PAL 内部识别码(如PAL_DEVICE_OUT_SPEAKER)。<back_end_name>: 配置重难点 !- 含义:连接 DSP 和硬件编码器的物理总线名称。
- 配置建议 :必须与 Kernel 中的设备树 (DTS) 匹配。例如
TDM-LPAIF_RXTX-RX-PRIMARY代表主 TDM 总线。
<snd_device_name>:- 含义:混音器路径名。
- 配置建议 :对应
mixer_paths.xml里的开关组名。
<usecase>与<devicePP-metadata>:- 含义:场景化算法注入。
- 配置建议 :通过
kvpair(Key-Value) 告诉 DSP 在特定场景加载什么算法。 - 例子 :
<kvpair key="0xAD000000" value="0xAD000002"/>。这里0xAD...02可能代表"单麦降噪算法"。如果你换了 4 麦阵列,需要将 Value 改为对应的 4 麦算法 ID。
5. 回声"和事佬":<in_streams>
解决"录音时听到自己播放的音乐"的问题(AEC 回声消除)。
<ec_ref>:- 含义:回声参考策略。
<disabled_stream>:- 含义:黑名单。
- 配置建议 :列在这里的流(如
PAL_STREAM_LOW_LATENCY)将不会被当作回声参考。例如,你不希望按键音触发回声消除算法导致录音断续,就把它加进来。
七、 配置检查清单 (Cheat Sheet)
| 任务 | 修改位置 | 关键标签 |
|---|---|---|
| 增加一个新扬声器 | <device_profile> |
<out-device> -> <id>, <back_end_name> |
| 提升通话降噪效果 | <in-device> |
<devicePP-metadata> -> 修改 value (算法 ID) |
| 解决蓝牙没声音 | <bt_codecs> |
检查 codec_library 是否正确加载 |
| 调整系统并发能力 | <config_params> |
max_sessions |
| 优化录音回声问题 | <in_streams> |
<ec_ref> -> <disabled_stream> |
八、 高级进阶:自定义 Key 与优先级策略
在复杂的座舱音频设计中,仅仅依靠设备 ID 匹配是不够的。RM 引入了 custom_key 和 priority 机制。
1. 自定义 Key (Custom Key) 匹配逻辑
当一个物理设备(如 Speaker)需要根据不同的软件场景(如:普通音乐 vs 导航提示音)加载不同的调音参数或后端配置时,会使用 custom_key。
源码分析 (getDeviceInfo):
cpp
// 如果传入了有效的 custom_key (来自上层配置)
for (int32_t k = 0; k < deviceInfo[i].usecase[j].config.size(); k++) {
if (!deviceInfo[i].usecase[j].config[k].key.compare(key)) {
// 发现匹配的 Custom Key 配置,强制覆盖之前的所有参数
if (deviceInfo[i].usecase[j].config[k].samplerate) {
devinfo->samplerate = deviceInfo[i].usecase[j].config[k].samplerate;
devinfo->samplerate_overwrite = true;
}
}
}
- 工程师建议 :如果你希望在导航播报时使用 16kHz 以节省功耗,而在听歌时使用 48kHz,可以通过为导航流设置一个特殊的
custom_key,并在 XML 中定义对应的config块来实现。
2. 优先级调度 (Priority Strategy)
当多个音频流同时使用同一个硬件 Backend 时,RM 需要决定最终的硬件参数(如采样率必须统一)。
决策原则:
- 高优先级优先:eCall (紧急呼叫) > Voice Call > Music。
- 采样率协商 :
updatePriorityAttr会遍历所有活动流,选取优先级最高的流所要求的采样率作为物理端口的最终采样率。
源码片段:
cpp
void ResourceManager::updatePriorityAttr(pal_device_id_t dev_id, ...) {
// 遍历所有当前正在运行的流 (activestreams)
for (auto const& stream_tuple : activestreams) {
Stream *stream = std::get<0>(stream_tuple);
// 获取该流在 XML 中定义的 priority (通过 getDeviceInfo 获取)
// 如果当前流优先级更高,则更新 newDevAttr 的配置
if (current_stream_priority > max_priority) {
max_priority = current_stream_priority;
newDevAttr->config.sample_rate = stream_sample_rate;
}
}
}
- 典型案例 :如果 44.1kHz 的音乐正在播放,此时插入一个 48kHz 的导航音且导航优先级更高,RM 会触发 Device Switch,将硬件后端重置为 48kHz,以确保导航音质不失真。
九、 总结:RM 的"三位一体"核心
- 静态声明 (XML):定义了系统支持的所有"可能性"。
- 动态匹配 (getDeviceInfo) :根据
Usecase + Device + CustomKey在可能性中寻找"最优解"。 - 冲突调解 (updatePriorityAttr):在多流并发时,通过"优先级"维护系统的最终稳定性。
xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<!-- Copyright (c) 2022 Qualcomm Technologies, Inc. -->
<!-- All Rights Reserved. -->
<!-- Confidential and Proprietary - Qualcomm Technologies, Inc.-->
<resource_manager_info>
<config_params>
<param key="native_audio_mode" value="multiple_mix_dsp"/>
<param key="max_sessions" value="128"/>
<param key="logging_level" value ="3" />
</config_params>
<config_gapless key="gapless_supported" value="1"/>
<bt_codecs>
<codec codec_format="CODEC_TYPE_AAC" codec_type="enc|dec" codec_library="lib_bt_bundle.so" />
<codec codec_format="CODEC_TYPE_SBC" codec_type="enc|dec" codec_library="lib_bt_bundle.so" />
<codec codec_format="CODEC_TYPE_LDAC" codec_type="enc" codec_library="lib_bt_bundle.so" />
<codec codec_format="CODEC_TYPE_APTX" codec_type="enc" codec_library="lib_bt_aptx.so" />
<codec codec_format="CODEC_TYPE_APTX_HD" codec_type="enc" codec_library="lib_bt_aptx.so" />
<codec codec_format="CODEC_TYPE_APTX_AD" codec_type="enc" codec_library="lib_bt_aptx.so" />
<codec codec_format="CODEC_TYPE_APTX_DUAL_MONO" codec_type="enc" codec_library="lib_bt_aptx.so" />
<codec codec_format="CODEC_TYPE_APTX_AD_SPEECH" codec_type="enc|dec" codec_library="lib_bt_aptx.so" />
</bt_codecs>
<controls>
<control name="PLUGIN_CONTROL_VOLUME" default="lib_default_plugin_controls.so" loadOnInit="true">
<plugin name="lib_default_plugin_controls.so" loadOnInit="true">
<plugin_usecase type="PAL_STREAM_LOW_LATENCY"/>
<plugin_usecase type="PAL_STREAM_DEEP_BUFFER"/>
<plugin_usecase type="PAL_STREAM_COMPRESSED"/>
<plugin_usecase type="PAL_STREAM_PLAYBACK_BUS"/>
<plugin_usecase type="PAL_STREAM_ASR_MIC"/>
<plugin_usecase type="PAL_STREAM_MIC_1CH"/>
<plugin_usecase type="PAL_STREAM_MIC_2CH"/>
<plugin_usecase type="PAL_STREAM_MIC_4CH"/>
<plugin_usecase type="PAL_STREAM_MIC_6CH"/>
<plugin_usecase type="PAL_STREAM_MIC_8CH"/>
<plugin_usecase type="PAL_STREAM_MIC_10CH"/>
<plugin_usecase type="PAL_STREAM_ECALL_DOWNLINK_MIC"/>
<plugin_usecase type="PAL_STREAM_ECALL_PLAYBACK"/>
<plugin_usecase type="PAL_STREAM_OUTSIDE"/>
<plugin_usecase type="PAL_STREAM_MIC_CP_SIRI"/>
<plugin_usecase type="PAL_STREAM_MIC_CP_SPEECH"/>
<plugin_usecase type="PAL_STREAM_SWITCH_TX"/>
<plugin_usecase type="PAL_STREAM_OUTSIDE_MIC"/>
</plugin>
</control>
<control name="PLUGIN_CONTROL_VOLUME_BOOST" default="lib_default_plugin_controls.so" loadOnInit="true"/>
<control name="PLUGIN_CONTROL_HD_VOICE" default="lib_default_plugin_controls.so" loadOnInit="true"/>
<control name="PLUGIN_CONTROL_AUDIO_BUFFER" default="lib_default_plugin_controls.so" loadOnInit="true">
<plugin name="lib_default_plugin_controls.so" loadOnInit="true">
<plugin_usecase type="PAL_STREAM_LOW_LATENCY"/>
<plugin_usecase type="PAL_STREAM_DEEP_BUFFER"/>
<plugin_usecase type="PAL_STREAM_COMPRESSED"/>
<plugin_usecase type="PAL_STREAM_PLAYBACK_BUS"/>
<plugin_usecase type="PAL_STREAM_VOIP_RX"/>
<plugin_usecase type="PAL_STREAM_PCM_OFFLOAD"/>
<plugin_usecase type="PAL_STREAM_ULTRA_LOW_LATENCY"/>
<plugin_usecase type="PAL_STREAM_ASR_MIC"/>
<plugin_usecase type="PAL_STREAM_MIC_2CH"/>
<plugin_usecase type="PAL_STREAM_MIC_4CH"/>
<plugin_usecase type="PAL_STREAM_MIC_6CH"/>
<plugin_usecase type="PAL_STREAM_MIC_8CH"/>
<plugin_usecase type="PAL_STREAM_MIC_10CH"/>
<plugin_usecase type="PAL_STREAM_ECALL_DOWNLINK_MIC"/>
<plugin_usecase type="PAL_STREAM_ECALL_PLAYBACK"/>
<plugin_usecase type="PAL_STREAM_OUTSIDE"/>
<plugin_usecase type="PAL_STREAM_MIC_CP_SIRI"/>
<plugin_usecase type="PAL_STREAM_MIC_CP_SPEECH"/>
<plugin_usecase type="PAL_STREAM_SWITCH_TX"/>
<plugin_usecase type="PAL_STREAM_OUTSIDE_MIC"/>
</plugin>
</control>
<control name="PLUGIN_CONTROL_AUDIO_LATENCY" default="lib_default_plugin_controls.so" loadOnInit="true">
<plugin name="lib_default_plugin_controls.so" loadOnInit="true">
<plugin_usecase type="PAL_STREAM_LOW_LATENCY"/>
<plugin_usecase type="PAL_STREAM_DEEP_BUFFER"/>
<plugin_usecase type="PAL_STREAM_COMPRESSED"/>
<plugin_usecase type="PAL_STREAM_PLAYBACK_BUS"/>
<plugin_usecase type="PAL_STREAM_PCM_OFFLOAD"/>
<plugin_usecase type="PAL_STREAM_ULTRA_LOW_LATENCY"/>
<plugin_usecase type="PAL_STREAM_ASR_MIC"/>
<plugin_usecase type="PAL_STREAM_MIC_2CH"/>
<plugin_usecase type="PAL_STREAM_MIC_4CH"/>
<plugin_usecase type="PAL_STREAM_MIC_6CH"/>
<plugin_usecase type="PAL_STREAM_MIC_8CH"/>
<plugin_usecase type="PAL_STREAM_MIC_10CH"/>
<plugin_usecase type="PAL_STREAM_ECALL_DOWNLINK_MIC"/>
<plugin_usecase type="PAL_STREAM_ECALL_PLAYBACK"/>
<plugin_usecase type="PAL_STREAM_OUTSIDE"/>
<plugin_usecase type="PAL_STREAM_MIC_CP_SIRI"/>
<plugin_usecase type="PAL_STREAM_MIC_CP_SPEECH"/>
<plugin_usecase type="PAL_STREAM_KTV_TX"/>
<plugin_usecase type="PAL_STREAM_SWITCH_TX"/>
<plugin_usecase type="PAL_STREAM_OUTSIDE_MIC"/>
</plugin>
</control>
</controls>
<config_voice>
<vsid>0xB3000000</vsid>
<mode_map>
<modepair key="0x11C05000" value="0xB3000001"/>
<modepair key="0x11DC5000" value="0xB3000001"/>
<modepair key="0x12006000" value="0xB3000001"/>
<modepair key="0x121C6000" value="0xB3000001"/>
</mode_map>
</config_voice>
<device_profile>
<in-device>
<id>PAL_DEVICE_IN_HANDSET_MIC</id>
<back_end_name>TDM-LPAIF_AUD-TX-PRIMARY</back_end_name>
<max_channels>16</max_channels>
<channels>16</channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<snd_device_name>handset-mic</snd_device_name>
<usecase>
<name>PAL_STREAM_LOW_LATENCY</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_DEEP_BUFFER</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_MIC_1CH</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_MIC_2CH</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_MIC_6CH</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_MIC_8CH</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD00F057"/>
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_MIC_CP_SIRI</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_MIC_CP_SPEECH</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_VOIP_TX</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000007"/>
<!-- VOIP_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_VOICE_UI</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000001"/>
<!-- VOICE_UI_FLUENCE_FFECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_VOICE_CALL</name>
<sidetone_mode>HW</sidetone_mode>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000008"/>
<!-- VOICE_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_LOOPBACK</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_ULTRA_LOW_LATENCY</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_ASR_MIC</id>
<back_end_name>TDM-LPAIF_AUD-TX-PRIMARY</back_end_name>
<max_channels>16</max_channels>
<channels>16</channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<snd_device_name>asr-mic</snd_device_name>
<usecase>
<name>PAL_STREAM_LOW_LATENCY</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
<usecase>
<name>PAL_STREAM_ASR_MIC</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_OUTSIDE_MIC</id>
<back_end_name>TDM-LPAIF_SDR-TX-PRIMARY</back_end_name>
<max_channels>16</max_channels>
<channels>16</channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<snd_device_name>outside-mic</snd_device_name>
<usecase>
<name>PAL_STREAM_OUTSIDE_MIC</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_KTV_MIC</id>
<back_end_name>TDM-LPAIF_AUD-TX-PRIMARY</back_end_name>
<max_channels>16</max_channels>
<channels>16</channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<snd_device_name>ktv-mic</snd_device_name>
<usecase>
<name>PAL_STREAM_KTV_TX</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_SWITCH</id>
<back_end_name>TDM-LPAIF-TX-PRIMARY</back_end_name>
<max_channels>8</max_channels>
<channels>8</channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<snd_device_name>switch-tx</snd_device_name>
<usecase>
<name>PAL_STREAM_SWITCH_TX</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_HFP_DOWNLINK</id>
<back_end_name>TDM-LPAIF_WSA-TX-PRIMARY</back_end_name>
<max_channels>8</max_channels>
<channels>8</channels>
<samplerate>16000</samplerate>
<snd_device_name>hfp-downlink</snd_device_name>
<bit_width>16</bit_width>
<usecase>
<name>PAL_STREAM_LOOPBACK</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_PROXY</id>
<back_end_name>PCM_RT_PROXY-TX-1</back_end_name>
<max_channels>2</max_channels>
<channels>1</channels>
<snd_device_name>afe-proxy</snd_device_name>
<usecase>
<name>PAL_STREAM_PROXY</name>
<devicePP-metadata>
</devicePP-metadata>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_FM_TUNER</id>
<back_end_name>TDM-LPAIF_SDR-TX-TERTIARY</back_end_name>
<max_channels>2</max_channels>
<channels>2</channels>
<snd_device_name>play-fm</snd_device_name>
<samplerate>48000</samplerate>
<bit_width>16</bit_width>
<usecase>
<name>PAL_STREAM_LOW_LATENCY</name>
</usecase>
</in-device>
<in-device>
<id>PAL_DEVICE_IN_ECALL_DOWNLINK</id>
<back_end_name>TDM-LPAIF_AUD-TX-PRIMARY</back_end_name>
<max_channels>16</max_channels>
<channels>16</channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<snd_device_name>ecall-mic</snd_device_name>
<usecase>
<name>PAL_STREAM_ECALL_DOWNLINK_MIC</name>
<devicePP-metadata>
<kvpair key="0xAD000000" value="0xAD000002"/>
<!-- AUDIO_FLUENCE_SMECNS -->
<!-- These key and value are derived from kvh2xml.h and should always be in sync with it -->
</devicePP-metadata>
</usecase>
</in-device>
<out-device>
<id>PAL_DEVICE_OUT_SPEAKER</id>
<back_end_name>TDM-LPAIF_RXTX-RX-PRIMARY</back_end_name>
<max_channels>16</max_channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<channels>16</channels>
<snd_device_name>speaker</snd_device_name>
<speaker_protection_enabled>0</speaker_protection_enabled>
<!-- RAS should be enabled with speaker protection always -->
<ras_enabled>0</ras_enabled>
<!-- Flag to specify speaker in case Voice call is switched from
handset to speaker with only 1 speaker.
Values: Both speaker : 0
Right speaker: 1
Left speaker : 2
-->
<speaker_mono_right>0</speaker_mono_right>
<!-- Time in seconds for Quick cal. Set 0 to disable -->
<quick_cal_time>0</quick_cal_time>
</out-device>
<out-device>
<id>PAL_DEVICE_OUT_SIDE_DEVICE</id>
<back_end_name>TDM-LPAIF-RX-TERTIARY</back_end_name>
<max_channels>8</max_channels>
<samplerate>48000</samplerate>
<bit_width>32</bit_width>
<channels>8</channels>
<snd_device_name>speaker</snd_device_name>
<speaker_protection_enabled>0</speaker_protection_enabled>
<!-- RAS should be enabled with speaker protection always -->
<ras_enabled>0</ras_enabled>
<!-- Flag to specify speaker in case Voice call is switched from
handset to speaker with only 1 speaker.
Values: Both speaker : 0
Right speaker: 1
Left speaker : 2
-->
<speaker_mono_right>0</speaker_mono_right>
<!-- Time in seconds for Quick cal. Set 0 to disable -->
<quick_cal_time>0</quick_cal_time>
</out-device>
<out-device>
<id>PAL_DEVICE_OUT_HFP_UPLINK</id>
<back_end_name>TDM-LPAIF_WSA-RX-PRIMARY</back_end_name>
<max_channels>8</max_channels>
<channels>8</channels>
<samplerate>16000</samplerate>
<snd_device_name>hfp-uplink</snd_device_name>
</out-device>
<out-device>
<id>PAL_DEVICE_OUT_PROXY</id>
<back_end_name></back_end_name>
<max_channels>8</max_channels>
<channels>2</channels>
<snd_device_name>afe-proxy</snd_device_name>
</out-device>
</device_profile>
<in_streams>
<in_stream>
<name>PAL_STREAM_VOIP_TX</name>
<policies>
<ec_ref>
<disabled_stream>PAL_STREAM_LOW_LATENCY</disabled_stream>
<disabled_stream>PAL_STREAM_GENERIC</disabled_stream>
</ec_ref>
</policies>
</in_stream>
<in_stream>
<name>PAL_STREAM_DEEP_BUFFER</name>
<policies>
<ec_ref>
<disabled_stream>PAL_STREAM_LOW_LATENCY</disabled_stream>
<disabled_stream>PAL_STREAM_GENERIC</disabled_stream>
</ec_ref>
</policies>
</in_stream>
<in_stream>
<name>PAL_STREAM_ASR_MIC</name>
<policies>
<ec_ref>
<disabled_stream>PAL_STREAM_LOW_LATENCY</disabled_stream>
<disabled_stream>PAL_STREAM_GENERIC</disabled_stream>
</ec_ref>
</policies>
</in_stream>
<in_stream>
<name>PAL_STREAM_VOICE_UI</name>
<policies>
<ec_ref>
<disabled_stream>PAL_STREAM_LOW_LATENCY</disabled_stream>
<disabled_stream>PAL_STREAM_GENERIC</disabled_stream>
</ec_ref>
</policies>
</in_stream>
<in_stream>
<name>PAL_STREAM_PROXY</name>
<policies>
<ec_ref>
<disabled_stream>PAL_STREAM_COMPRESSED</disabled_stream>
<disabled_stream>PAL_STREAM_ULTRA_LOW_LATENCY</disabled_stream>
<disabled_stream>PAL_STREAM_LOW_LATENCY</disabled_stream>
<disabled_stream>PAL_STREAM_GENERIC</disabled_stream>
</ec_ref>
</policies>
</in_stream>
</in_streams>
</resource_manager_info>