使用 libaudioclient 实现 Android Native层 音频测试工具
使用 libaudioclient 实现 Android Native层 音频测试工具
Qidi Huang 2025.09.18
原本以为只要是做过两年 Android Audio HAL 开发工作的工程师,都熟悉使用 libaudioclient 来编写测试程序的方式,但从近两家公司来看并非如此。所以还是把写法在这里贴一下吧,免得以后又重写。
项目 bring up 阶段通常需要测试各模块基本功能。对 audio HAL 模块来说,一般要求实现以下几个目标:
- 多媒体音源出声
- 能调节音量
- 能设置静音
通常在芯片厂提供基线代码时,公版硬件就支持多媒体音源出声了。后续要在公司自己的硬件上出声,工作量主要在 audio BSP 部分,audio HAL 部分一般不需要改什么,也不用编写测试工具。随便安装一个 音乐APP 放首歌就行。
截止 Android 16,音量调节还是通过 setAudioPortConfig() 实现的,所以我们需要构造 AudioPortConfig 参数。由于 Android 版本演进,AudioPortConfig 参数先后出现了 3 个版本:
- 结构体版本:
audio_port_config - HIDL版本:
::android::hardware::audio::common::V7_0::AudioPortConfig - AIDL版本:
::aidl::android::media::audio::common::AudioPortConfig
所以如果在测试工具代码里直接调用 audio HAL 接口来调节音量,比如调用 IDevice::setAudioPortConfig() 或 IModule::setAudioPortConfig(),就意味着必须根据 Android 版本来修改代码以使用对应的 AudioPortConfig,并对参数进行初始化。当你需要在多个芯片平台、多个 Android 版本上进行测试时,这将是个灾难。 尽管 Android 提供了 HidlUtils::audioPortConfigToHal() 和 aidl::android::legacy2aidl_audio_port_config_AudioPortConfig() 这样的转换函数,用于将上述 3 个版本的 AudioPortConfig 参数相互转换,但留给工程师的工作量依然不小。很不幸,我卖身的近两家公司,都是采用的这种方式,这导致工程师不得不反复进行参数转换、修改大堆变量值(比如 portId、address),以达到调节不同音源音量的目的。
人生短暂,把时间浪费在重复编写这些琐碎的测试代码上让人感到悲哀。所以为什么不使用 libaudioclient 呢?它让你不需要关注这些细枝末节的差别,一份代码可以不加修改地完美运行在不同芯片平台、不同 Android 版本上。
使用 libaudioclient 实现音量调节。测试代码如下:
// main.cpp
#include <string>
#include <stdlib.h>
#include <stdio.h>
#include <system/audio.h>
#include <media/AudioSystem.h>
std::string portRole(audio_port_role_t role)
{
std::string ret{};
switch (role) {
case AUDIO_PORT_ROLE_NONE:
ret = "NONE";
break;
case AUDIO_PORT_ROLE_SOURCE:
ret = "SOURCE";
break;
case AUDIO_PORT_ROLE_SINK:
ret = "SINK";
break;
default:
break;
}
return ret;
}
std::string portType(audio_port_type_t type)
{
std::string ret{};
switch (type) {
case AUDIO_PORT_TYPE_NONE:
ret = "NONE";
break;
case AUDIO_PORT_TYPE_MIX:
ret = "MIX";
break;
case AUDIO_PORT_TYPE_DEVICE:
ret = "DEVICE";
break;
case AUDIO_PORT_TYPE_SESSION:
ret = "SESSION";
break;
default:
break;
}
return ret;
}
void setBusVolume()
{
unsigned int numPorts = 100, generation = 0;
struct audio_port_v7 *ports = (struct audio_port_v7 *)calloc(1, sizeof(struct audio_port_v7) * numPorts);
auto retList = android::AudioSystem::listAudioPorts(AUDIO_PORT_ROLE_SINK, AUDIO_PORT_TYPE_DEVICE, &numPorts, ports, &generation);
printf("numPorts(%u), generation(%u)\n", numPorts, generation);
if (retList != android::OK) {
printf("ERROR: unable to list ports. Is audio subsystem ready?\n");
return;
}
for (int i=0; i<numPorts; ++i) {
printf("%2d. role(%s), type(%s), address(%s)\n",
i,
portRole(ports[i].role).c_str(),
portType(ports[i].type).c_str(),
ports[i].type == AUDIO_PORT_TYPE_DEVICE? ports[i].ext.device.address: ports[i].name
);
}
unsigned int userInput = -1;
while (userInput<0 || userInput>=numPorts) {
printf("select a bus [0, %u]\n", numPorts-1);
scanf("%u", &userInput);
}
struct audio_port_v7 *selectedPortV7 = &ports[userInput];
int userGain = 1;
while (userGain<-7800 || userGain>0) {
printf("specify gain value [-7800, 0]: \n");
scanf("%d", &userGain);
}
selectedPortV7->active_config.gain.values[0] = userGain;
auto retSet = android::AudioSystem::setAudioPortConfig(&(selectedPortV7->active_config));
if (retSet != android::OK) {
printf("setBusVolume failed.\n");
return;
}
printf("setBusVolume done.\n");
free(ports);
}
int main()
{
setBusVolume();
return 0;
}
Makefile 如下(Android.mk 实现。使用 androidmk 工具可转换为 Android.bp):
include $(CLEAR_VARS)
LOCAL_MODULE := MyAudioTest
LOCAL_SRC_FILES := main.cpp
LOCAL_SHARED_LIBRARIES := libutils libaudioclient
LOCAL_HEADER_LIBRARIES := libaudio_system_headers
LOCAL_MULTILIB := 64
LOCAL_VENDOR_MODULE := false
include $(BUILD_EXECUTABLE)
libaudioclient 除了支持 setAudioPortConfig() 调用,也支持 setMasterMute()、setStreamMute()、setParameters()、getParameters()、setMode() 等接口调用,满足各种开发测试需求。
多提一嘴。
在 HIDL 还未废弃时,有的公司在 audio HAL 层使用 IDevice::setParameters() 接口来实现设置静音的功能。该接口的参数是hidl_vec<ParameterValue>。这些公司通常会在 audio HAL 里加入自己的代码,将参数转换成 AudioParameter 或 键值对字符串,再对其进行判断看是否与静音功能有关。
然而,由于 Google 决定废弃 HIDL,在新版本 Android 中,越来越多 HAL 转而以 AIDL 实现。从 Android 16 起,Qualcomm 也在其基线代码中实现了 AIDL 版本的 audio HAL,IDevice::setParameters() 接口也被替换成了 IModule::setVendorParameters(),新接口的参数变成了vector<VendorParameter>。这意味着那些公司的代码无法直接使用了。
这些公司要么也全面修订自己的代码,投入 AIDL 怀抱;要么将 AIDL 参数转换成老参数,继续使用遗留代码。显然,多数公司会选择后者。
根据 Android 设计,存在 IHalAdapterVendorExtension 接口(需要我们或者芯片厂实现)。该接口中的 processVendorParameters() 方法能够将 vector<VendorParameter> 转换为 字符串 ;另一个 parseVendorParameters() 方法则能够将 字符串 转换为 vector<VendorParameter> 。而 AudioParameter 本身就具备与 字符串 相互转换的能力。 因此,新旧参数的问题成功解决,公司遗留代码的命也续上了。
参数转换代码如下:
ndk::ScopedAStatus ModulePrimary::setVendorParameters(
const std::vector<::aidl::android::hardware::audio::core::VendorParameter>& in_parameters,
bool in_async) {
......
std::string kvString{};
mHalAdapterVendorExtn.processVendorParameters(ParameterScope::MODULE, in_parameters, &kvString);
LOG_I("AHAL received VendorParameters(%s)", kvString.c_str());
AudioParameter params(String8(kvString.c_str()));
your_legacy_hal_set_parameters(params);
......
return ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus ModulePrimary::getVendorParameters(
const std::vector<std::string>& in_ids,
std::vector<::aidl::android::hardware::audio::core::VendorParameter>* _aidl_return) {
......
auto ids = in_ids;
AudioParameter paramKeys(String8(vectorToString(ids).c_str()));
AudioParameter paramReplys{};
your_legacy_hal_get_parameters(paramKeys, paramReplys);
std::string paramReplyStr{paramReplys.toString().c_str()};
std::vector<VendorParameter> syncParameters, asyncParameters;
mHalAdapterVendorExtn.parseVendorParameters(
ParameterScope::MODULE,
paramReplyStr,
&syncParameters,
&asyncParameters);
// Currently only support syncParameters
std::move(syncParameters.begin(), syncParameters.end(), std::back_inserter(*_aidl_return));
......
return ndk::ScopedAStatus::ok();
}
尽管 IHalAdapterVendorExtension 被注册为 AIDL service,但这段代码没有通过 binder 去获取服务,而是通过链接 IHalAdapterVendorExtension 实现端的库,直接在代码里构造了 mHalAdapterVendorExtn 对象,以避免不必要的跨进程通信开销。
本文来自博客园,作者:Qidi_Huang,转载请注明原文链接:https://www.cnblogs.com/qidi-huang/p/19099541

浙公网安备 33010602011771号