不使用 AAudio 的前提下,降低 Android 音频传输延时的方案

Qidi Huang 2025.11.01


0. 前言

车载 Android 系统,往往需要支持音频投射(Audio Projection)、手机车机互联(如 Carplay、AndroidAuto、HiCar、CarLife、ICCOA)等功能。

要通过 Carplay 等等认证,则必须使设备满足一系列要求,比如音频数据传输延时小于 200 毫秒。

为实现这一目标,Google 推荐使用 AAudio 方案,见官网文档 High-performance audioAAudio and MMAP

采用 AAudio 方案能降低数据传输延时的原因在于:

  • 数据不经过 AudioFlinger 混音;
  • 数据不写入 Audio HAL。

但因此也带来两个局限:

  • 依赖于芯片厂实现 MMAP 驱动接口;
  • 音频数据直接在 Frameworks 和 Driver 之间传输,不经过 audio HAL,所以无法将自定义音效部署在 audio HAL 层。

如果芯片厂未实现 MMAP 驱动接口,那么即使客户端代码调用了 AAudio 接口,也无法降低音频传输延时。因为 AAudio 会回退到以 legacy 模式工作,也就是通过 AudioTrack -> AudioFlinger -> AudioHAL -> Driver 这条路径传输数据。

在芯片驱动不支持使用 AAudio 的前提下,我们可以通过其它方式来减少音频传输延时。


1. 使用 DeviceHalAidl

在 native 层实现一个中间件,向上对 priv-appjava-service 提供 AIDL 服务接口;向下通过 DeviceHalAidl 打开 StreamHalAidl 来向/从 AudioHAL 写入/读取音频数据。以此绕过 AudioTrackAudioFlinger 部分的延时。

DeviceHalAidlStreamHalAidl 等组件属于 libaudiohal。不过要注意, 在使用 libaudiohal 时,不能引用 libaudiohal_default 模块,因为其配置参数中包含 -fvisibility=hidden,这会导致任何引用了该模块的源码都不导出符号表, 从而编译失败,并提示 undefined symbol 一类错误。

这是我推荐的方式。使用 DeviceHalAidl 的示例代码如下。

AudioTest.h 头文件:

#include <android/binder_auto_utils.h>
#include <aidl/android/hardware/audio/core/IModule.h>
#include <aidl/android/media/audio/IHalAdapterVendorExtension.h>
#include <aidl/vendor/xxxx/service/IXxxxService.h>

using ::aidl::android::hardware::audio::core::IModule;
using ::aidl::android::media::audio::IHalAdapterVendorExtension;
using ::aidl::vendor::xxxx::service::IXxxxService;
using ::ndk::SpAIBinder;
using ::ndk::ScopedAIBinder_DeathRecipient;
using aidl::android::hardware::audio::core::VendorParameter;

using ParameterScope = aidl::android::media::audio::IHalAdapterVendorExtension::ParameterScope;

class AudioTest {
    SpAIBinder mpAhalBinderObj;
    std::shared_ptr<IModule> mpAhalModule = nullptr;
    std::atomic<bool> bIsAhalConnected;
    ndk::SpAIBinder mpVendorExtBinderObj;
    std::shared_ptr<IHalAdapterVendorExtension> mpHalAdapterVendorExt = nullptr;
    ndk::SpAIBinder mpXxxxSvcBinderObj;
    std::shared_ptr<IXxxxService> mpXxxxService = nullptr;
    ScopedAIBinder_DeathRecipient mDeathRecipient;

    static void onBinderDied(void *cookie);
    void onBinderDiedImpl();
    int connectServices();

 public:
    enum class CarplayRequest : unsigned char {
        PLAY = 0,
        RECORD
    };

    void carplayTest(CarplayRequest request);
    void setParameters(std::string strKeyVal);
};

AudioTest.cpp 源文件:

#include <chrono>
#include <thread>
#include <utils/Log.h>
#include <DeviceHalAidl.h>
#include <StreamHalAidl.h>

using ::android::sp;
using ::android::DeviceHalAidl;
using ::android::StreamOutHalInterface;
using ::android::StreamInHalInterface;
using ::android::status_t;

void AudioTest::onBinderDied(void *cookie) {
    auto thiz = static_cast<AudioTest*>(cookie);
    thiz->onBinderDiedImpl();
}

void AudioTest::onBinderDiedImpl() {
    AIBinder_unlinkToDeath(mpAhalBinderObj.get(), mDeathRecipient.get(), this);
    mpAhalBinderObj.set(nullptr);
    mpAhalModule = nullptr;
}

AudioTest::AudioTest() :
      mDeathRecipient(AIBinder_DeathRecipient_new(onBinderDied))
{}

int AudioTest::connectServices()
{
    if (!mpAhalModule) {
        auto ahalSvcName = std::string(IModule::descriptor) + "/default";
        mpAhalBinderObj.set(AServiceManager_checkService(ahalSvcName.c_str()));
        if (nullptr == mpAhalBinderObj.get()) {
            ALOGE("Failed connecting AHAL service %s", ahalSvcName.c_str());
            return -1;
        }
        AIBinder_linkToDeath(mpAhalBinderObj.get(), mDeathRecipient.get(), this);
        mpAhalModule = IModule::fromBinder(mpAhalBinderObj);
    }
    if (!mpHalAdapterVendorExt) {
        auto halAdapterExtSvcName = std::string(IHalAdapterVendorExtension::descriptor) + "/default";
        mpVendorExtBinderObj.set(AServiceManager_checkService(halAdapterExtSvcName.c_str()));
        if (nullptr == mpVendorExtBinderObj.get()) {
            ALOGE("Failed connecting service %s", halAdapterExtSvcName.c_str());
            return -1;
        }
        AIBinder_linkToDeath(mpVendorExtBinderObj.get(), mDeathRecipient.get(), this);
        mpHalAdapterVendorExt = IHalAdapterVendorExtension::fromBinder(mpVendorExtBinderObj);
    }
    if (!mpXxxxService) {
        auto xxxxSvcName = std::string(IXxxxService::descriptor) + "/default";
        mpXxxxSvcBinderObj.set(AServiceManager_checkService(xxxxSvcName.c_str()));
        if (nullptr == mpXxxxSvcBinderObj.get()) {
            ALOGE("Failed connecting service %s", xxxxSvcName.c_str());
            return -1;
        }
        AIBinder_linkToDeath(mpXxxxSvcBinderObj.get(), mDeathRecipient.get(), this);
        mpXxxxService = IXxxxService::fromBinder(mpXxxxSvcBinderObj);
    }
    return 0;
}

void AudioTest::carplayTest(CarplayRequest request)
{
    connectServices();

    sp<DeviceHalAidl> aidlAudioDev = sp<DeviceHalAidl>::make("carplayTest", mpAhalModule, mpHalAdapterVendorExt);
    status_t retInit = aidlAudioDev->initCheck();
    if (retInit != android::OK) {
        ALOGE("ERROR: uninitialized audio device.");
        return;
    }

    if (CarplayRequest::PLAY == request) {
        sp<StreamOutHalInterface> carplayStreamOut;
        struct audio_config carplayConfig = AUDIO_CONFIG_INITIALIZER;
        carplayConfig.offload_info.stream_type = AUDIO_STREAM_ASSISTANT;
        carplayConfig.sample_rate = 16000;
        carplayConfig.channel_mask = AUDIO_CHANNEL_OUT_STEREO;
        carplayConfig.format = AUDIO_FORMAT_PCM_16_BIT;

        status_t ret = aidlAudioDev->openOutputStream(
                            AUDIO_IO_HANDLE_NONE,
                            AUDIO_DEVICE_OUT_BUS,
                            AUDIO_OUTPUT_FLAG_PROJECTION_VR,
                            &carplayConfig,
                            "BUS07_PROJECTION_VR_OUT",
                            &carplayStreamOut);
        if (ret != android::OK) {
            ALOGE("failed opening carplay output stream.");
            return;
        }

        android::String8 keys{"getPcmId"}, values{}, retPcmId{};
        carplayStreamOut->getParameters(keys, &values);
        android::AudioParameter replyParams{values};
        replyParams.get(keys, retPcmId);
        if (retPcmId.empty()) {
            ALOGE("Playback get PcmId failed.\n");
            return;
        }

        IXxxxService::periodInfo info{0, 0};
        int32_t retGetPeriod = -1;
        mpXxxxService->gwm_get_pcm_period(retPcmId.c_str(), &info, &retGetPeriod);
        if (retGetPeriod != 0) {
            ALOGE("Playback gwm_get_pcm_period failed.");
            return;
        }
        uint32_t period_size = info.period_size;
        uint32_t period_count = info.period_count;


        char fileName[] = "/data/TheHistory.pcm";
        FILE *fp = fopen(fileName, "rb");
        if (!fp) {
            ALOGE("Failed opening %s: %s", fileName, strerror(errno));
            return;
        }
        ALOGD("Successfully opened file: %s", fileName);

        int bufferSize = 2 /* channelMask */ * sizeof(int16_t) * period_size;
        unsigned char *buf = (unsigned char *)malloc(bufferSize);
        if (!buf) {
            ALOGE("buff malloc failed");
            return;
        }

        size_t readSize = 0;
        size_t writtenSize = 0;

        while (0 == feof(fp)) {
            readSize = fread(buf, 1, bufferSize, fp);
            if (readSize <= 0) {
                ALOGD("fread data failed, readSize = %zu", readSize);
                break;
            }
            ALOGD("read(%zu) from file.", readSize);
            carplayStreamOut->write(buf, bufferSize, &writtenSize);
            ALOGD("written(%zu) to device.", writtenSize);
        }
        if (buf) {
            free(buf);
            buf = nullptr;
        }
        if (fp) {
            fclose(fp);
            fp = nullptr;
        }

    } else if (CarplayRequest::RECORD == request) {
        sp<StreamInHalInterface> carplayStreamIn;
        struct audio_config carplayConfig = AUDIO_CONFIG_INITIALIZER;
        carplayConfig.offload_info.stream_type = AUDIO_STREAM_VOICE_CALL;
        carplayConfig.sample_rate = 16000;
        carplayConfig.channel_mask = AUDIO_CHANNEL_IN_MONO;
        carplayConfig.format = AUDIO_FORMAT_PCM_16_BIT;

        status_t retOpenStream = aidlAudioDev->openInputStream(
                                    AUDIO_IO_HANDLE_NONE,
                                    AUDIO_DEVICE_IN_BUS,
                                    &carplayConfig,
                                    AUDIO_INPUT_FLAG_PROJECTION_VR,  // TODO: config->flags
                                    "BUS01_PROJECTION_VR_IN",  // TODO: address.c_str()
                                    AUDIO_SOURCE_CP_ESIRI,  // TODO: dynamical assignment
                                    AUDIO_DEVICE_NONE,  // TODO: dynamical assignment. Can be unspecified, per RecordTrackMetadata.aidl comments.
                                    "",  // TODO: dynamical assignment
                                    &carplayStreamIn);
        if (retOpenStream != android::OK) {
            ALOGE("Failed opening carplay input stream.");
            return;
        }

        android::String8 keys{"getPcmId"}, values{}, retPcmId{};
        carplayStreamIn->getParameters(keys, &values);
        android::AudioParameter replyParams{values};
        replyParams.get(keys, retPcmId);
        if (retPcmId.empty()) {
            ALOGE("Record stream(%p) get PcmId failed.", carplayStreamIn.get());
            return;
        }

        IXxxxService::periodInfo info{0, 0};
        int32_t retGetPeriod = -1;
        mpXxxxService->gwm_get_pcm_period(retPcmId.c_str(), &info, &retGetPeriod);
        if (retGetPeriod != 0) {
            ALOGE("Playback gwm_get_pcm_period failed.");
            return;
        }
        uint32_t period_size = info.period_size;
        uint32_t period_count = info.period_count;


        char fileName[] = "/data/record_16K_2CH_16B.pcm";
        FILE *fp = fopen(fileName, "wb");
        if (!fp) {
            ALOGE("Failed opening %s: %s", fileName, strerror(errno));
            return;
        }
        ALOGD("Successfully opened file: %s", fileName);

        using namespace std::literals::chrono_literals;
        auto startTime = std::chrono::steady_clock::now();
        auto curTime = startTime;
        auto endTime = startTime + 30s;

        int bufferSize = 2 /* channelMask */ * sizeof(int16_t) * period_size;
        unsigned char *buf = (unsigned char *)malloc(bufferSize);
        if (!buf) {
            ALOGE("buff malloc failed");
            return;
        }

        size_t readSize = 0;
        size_t writtenSize = 0;

        while (curTime < endTime) {
            carplayStreamIn->read(buf, bufferSize, &readSize);
            ALOGD("read(%zu) from device", readSize);
            writtenSize = fwrite(buf, bufferSize, 1, fp);
            ALOGD("written(%zu) to file.", bufferSize * writtenSize);
            curTime = std::chrono::steady_clock::now();
        }
        if (buf) {
            free(buf);
            buf = nullptr;
        }
        if (fp) {
            fclose(fp);
            fp = nullptr;
        }

    } else {
        ALOGE("Unsupported carplay request: %u", static_cast<unsigned char>(request));
    }
}

void AudioTest::setParameters(std::string strKeyVal)
{
    ALOGD("strKeyVal: %s", strKeyVal.c_str());
    if (strKeyVal.empty()) return;

    connectServices();

    std::vector<VendorParameter> syncParameters, asyncParameters;
    mpHalAdapterVendorExt->parseVendorParameters(
                                ParameterScope::MODULE,
                                strKeyVal,
                                &syncParameters,
                                &asyncParameters);
    if (!syncParameters.empty() && mpAhalModule) {
        auto ret = mpAhalModule->setVendorParameters(syncParameters, false);
        if (!ret.isOk()) {
            ALOGE("Failed sending cmds(%s) to IModule service.", strKeyVal.c_str());
            return;
        }
    }
    ALOGD("setParameters done %zu!", syncParameters.size());
}


Android.bp 文件:

cc_library_shared {
    name: "AudioTest",
    defaults: [
        "libaudiohal_aidl_default",
    ],
    srcs: [
        ":core_audio_hal_aidl_src_files",
        "AudioTest.cpp",
    ],
    local_include_dirs:[
        "include",
    ],
    export_include_dirs: [
        "include",
    ],
    header_libs: [
        "libaudio_system_headers",
        "libaudiohalimpl_headers",
    ],
    shared_libs: [
        "audioclient-types-aidl-cpp",
        "av-types-aidl-cpp",
        "libaudioclient_aidl_conversion",
        "libaudiofoundation",
        "libaudioutils",
        "libbase",
        "libbinder",
        "libcutils",
        "libfmq",
        "libhardware",
        "liblog",
        "libmedia_helper",
        "libmediautils",
        "libutils",
        "libbinder_ndk",
        "libaudiohal",
        "av-audio-types-aidl-ndk",
        "android.media.audio.common.types-V4-cpp",
        "android.hardware.audio.core-V3-ndk",
        "vendor.xxxx.service-V1-ndk"
    ],
    cppflags: [
        "-Wall",
        "-Werror",
		"-Wno-error=sign-compare",
        "-fexceptions",
    ],
    vendor_available: true,
}

audio_module_config_primary.xml 配置文件:

......
<module name="default">
    <attachedDevices>
        <item>Projection Vr Out Bus</item>
        ......
        <item>Projection Vr In Bus</item>
    </attachedDevices>
    ......
    <mixPorts>
        <mixPort name="projection_vr_out" role="source" maxOpenCount="2" maxActiveCount="2" flags="PROJECTION_VR">
            <profile samplingRates="16000 48000" channelLayouts="LAYOUT_MONO LAYOUT_STEREO" formatType="PCM" pcmType="INT_16_BIT" />
        </mixPort>
        ......
        <mixPort name="projection_vr_in" role="sink" maxOpenCount="2" maxActiveCount="2" flags="PROJECTION_VR">
            <profile samplingRates="16000" channelLayouts="LAYOUT_MONO" formatType="PCM" pcmType="INT_16_BIT" />
        </mixPort>
    </mixPorts>
    <devicePorts>
        <devicePort tagName="Projection Vr Out Bus" role="sink" attached="true" deviceType="OUT_BUS" address="BUS07_PROJECTION_VR_OUT">
            <profile samplingRates="48000" channelLayouts="LAYOUT_MONO LAYOUT_STEREO" formatType="PCM" pcmType="INT_16_BIT" />
            <gains>
                <gain name="gain_1" mode="JOINT"
                      minValueMB="-7800"
                      maxValueMB="0"
                      defaultValueMB="-5800"
                      minRampMs="10"
                      maxRampMs="100"
                      stepValueMB="200"/>
            </gains>
        </devicePort>
        ......
        <devicePort tagName="Projection Vr In Bus" deviceType="IN_BUS" role="source" attached="true" address="BUS01_PROJECTION_VR_IN">
            <profile samplingRates="16000" channelLayouts="LAYOUT_MONO" formatType="PCM" pcmType="INT_16_BIT" />
        </devicePort>
    </devicePorts>
    <routes>
        <route type="mix" sink="Projection Vr Out Bus" sources="projection_vr_out" />
        ......
        <route type="mix" sink="projection_vr_in" sources="Projection Vr In Bus"/>
    </routes>
</module>

2. 使用 hidl_memory

尽管 Google 正在逐步废弃 HIDL 机制,但如果你的设备上运行的是 Android 15 或更早的版本,这些版本的系统依然完好支持 HIDL,所以可以选择使用 hidl_memory 来向/从 AudioHAL 写入/读取音频数据。

考虑到新版本 Android 上无法使用,我不推荐这种方式。代码略过。

posted @ 2025-11-01 16:44  Qidi_Huang  阅读(43)  评论(0)    收藏  举报