使用 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 := falseinclude $(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 syncParametersstd::move(syncParameters.begin(), syncParameters.end(), std::back_inserter(*_aidl_return));......return ndk::ScopedAStatus::ok();
}
尽管 IHalAdapterVendorExtension
被注册为 AIDL service,但这段代码没有通过 binder 去获取服务,而是通过链接 IHalAdapterVendorExtension
实现端的库,直接在代码里构造了 mHalAdapterVendorExtn
对象,以避免不必要的跨进程通信开销。