不使用 AAudio 的前提下,降低 Android 音频传输延时的方案
Qidi Huang 2025.11.01
0. 前言
车载 Android 系统,往往需要支持音频投射(Audio Projection)、手机车机互联(如 Carplay、AndroidAuto、HiCar、CarLife、ICCOA)等功能。
要通过 Carplay 等等认证,则必须使设备满足一系列要求,比如音频数据传输延时小于 200 毫秒。
为实现这一目标,Google 推荐使用 AAudio 方案,见官网文档 High-performance audio 和 AAudio 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-app 或 java-service 提供 AIDL 服务接口;向下通过 DeviceHalAidl 打开 StreamHalAidl 来向/从 AudioHAL 写入/读取音频数据。以此绕过 AudioTrack 和 AudioFlinger 部分的延时。
DeviceHalAidl、StreamHalAidl 等组件属于 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 上无法使用,我不推荐这种方式。代码略过。
本文来自博客园,作者:Qidi_Huang,转载请注明原文链接:https://www.cnblogs.com/qidi-huang/p/19183517

浙公网安备 33010602011771号