使用 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 对象,以避免不必要的跨进程通信开销。

posted @ 2025-09-18 19:25  Qidi_Huang  阅读(72)  评论(0)    收藏  举报