新闻详情

新闻详情

首页 / 资讯中心 / 详情

RK3588 Android 12音频定制:HDMI与喇叭同步发声的tinyalsa_hal实现

发布时间:2026/9/28 18:01:23来源:尧图网络
RK3588 Android 12音频定制:HDMI与喇叭同步发声的tinyalsa_hal实现
1. 项目背景与需求拆解RK3588这颗芯片在音视频领域的热度一直居高不下四核A76加四核A55的架构配上Mali-G610的GPU和独立的NPU让它成了不少高端盒子、一体机、工控设备、商显终端的首选方案。我手上这块板子跑的是Android 12硬件上配了两路HDMI输出加一路板载喇叭客户的需求很直接插上HDMI之后HDMI和喇叭要能同时出声而不是像默认行为那样HDMI一插上就把喇叭静音了。这个需求听起来简单但真正动起手来才发现Android原生的音频策略框架里HDMI和Speaker被定义成了互斥的输出设备。系统默认的优先级逻辑是当检测到HDMI插入时自动把音频路由切到HDMISpeaker被强制静音。这套逻辑在手机和平板上是合理的毕竟用户插了HDMI就是想用电视或显示器出声。但在商显、广告机、会议一体机这类场景里经常需要HDMI输出给大屏的同时本地喇叭也要同步发声比如做双屏异显加本地提示音或者需要HDMI和喇叭形成声场叠加。要改这个行为绕不开的核心模块就是tinyalsa_hal。这是Rockchip平台在Android上使用的音频硬件抽象层负责把上层AudioFlinger的请求翻译成底层ALSA驱动的具体操作。它决定了音频走哪条通路、混音怎么处理、设备切换的时机和策略。网上关于tinyalsa_hal的资料比较零散大部分是讲怎么编译、怎么加日志真正涉及双HDMI加喇叭同步发声这种具体场景的完整修改方案并不多。我把自己踩过的坑和最终跑通的方案整理出来给遇到同样问题的朋友一个可直接参考的路径。这篇文章适合几类人看一是正在做RK3588 Android 12音频定制的驱动工程师二是需要实现多路音频同时输出的产品开发者三是对Android音频HAL层感兴趣、想了解tinyalsa_hal内部机制的技术人员。即使你用的是RK3576或者其他Rockchip平台思路也是相通的只是寄存器地址和配置文件路径可能有差异。2. tinyalsa_hal架构与音频路由机制解析2.1 tinyalsa_hal在Android音频栈中的位置Android的音频架构从上到下大致分四层应用层的AudioTrack/AudioRecord框架层的AudioFlinger和AudioPolicyService硬件抽象层的audio.primary.xxx.so以及最底层的ALSA驱动和codec芯片。tinyalsa_hal就处在HAL这一层编译产物通常是audio.primary.rk3588.so放在/vendor/lib/hw/或/vendor/lib64/hw/目录下。它对上要实现Android定义的hardware/audio.h接口包括open_output_stream、start_output_stream、out_write这些关键函数对下要通过tinyalsa库的pcm_open、pcm_write、mixer_ctl_set_value等接口操作具体的声卡设备。RK3588平台上通常有多个声卡card 0一般是板载codec比如ES8388、RK809内置codeccard 1和card 2可能是HDMI音频控制器。每张声卡下面有多个PCM设备对应不同的输出通路。理解这个层级关系很重要因为我们要改的“HDMI和喇叭同时发声”本质上是在HAL层决定当上层要求播放音频时是只打开一个PCM设备还是同时打开多个PCM设备并写入相同的数据。2.2 Android原生音频路由的互斥逻辑Android的AudioPolicyManager里有一套设备选择策略核心逻辑在getDeviceForStrategy函数中。对于STRATEGY_MEDIA这种媒体播放策略系统会按照优先级从高到低检查可用设备HDMI 有线耳机 蓝牙A2DP Speaker。一旦HDMI被标记为可用通过AUDIO_DEVICE_OUT_HDMISpeaker就会被排除在候选列表之外。这个策略的出发点是避免声音从多个设备同时出来造成回声或延迟差异。但在我们的场景里HDMI和Speaker是物理上独立的输出通路不存在回声问题反而需要它们同步工作。所以修改分两个层面一是让AudioPolicy认为这两个设备可以同时被选中二是在HAL层真正实现同时向两个PCM设备写数据。注意直接改AudioPolicyManager的代码在Android 12上比较麻烦因为它是framework层的核心模块重新编译system分区镜像的工作量大而且容易引入其他兼容性问题。更稳妥的做法是在HAL层做文章让HAL在收到HDMI输出请求时内部同时打开Speaker通路。2.3 RK3588音频通路的硬件拓扑RK3588的音频子系统比较复杂它内部有多个I2S控制器、SPDIF控制器和HDMI音频模块。以我手上这块板子为例硬件连接大致是这样的I2S0连接到板载codecES8388负责喇叭输出I2S1连接到第一路HDMI音频通过HDMI TX0I2S2连接到第二路HDMI音频通过HDMI TX1SPDIF可能用于光纤输出这里没用到在ALSA层面对应的声卡和PCM设备编号需要在/vendor/etc/audio_policy_configuration.xml和HAL的配置里确认。你可以通过cat /proc/asound/cards查看声卡列表通过cat /proc/asound/devices查看PCM设备编号。我板子上的实际输出是# cat /proc/asound/cards 0 [rockchipes8388 ]: rockchip-es8388 - rockchip-es8388 rockchip-es8388 1 [rockchiphdmi0 ]: rockchip-hdmi0 - rockchip-hdmi0 rockchip-hdmi0 2 [rockchiphdmi1 ]: rockchip-hdmi1 - rockchip-hdmi1 rockchip-hdmi1这意味着喇叭走card 0两路HDMI分别走card 1和card 2。HAL层需要根据上层传来的设备类型决定打开哪个card的哪个device。3. 修改方案设计与关键代码实现3.1 整体修改思路我的方案是在tinyalsa_hal的output stream打开逻辑里做扩展。具体来说当上层请求的输出设备包含AUDIO_DEVICE_OUT_HDMI时HAL不仅打开HDMI对应的PCM设备还额外打开Speaker对应的PCM设备并在out_write函数里把同一份音频数据同时写入两个PCM句柄。这样做的好处是改动集中在HAL层不需要动framework编译和替换都方便。缺点是HAL层需要自己维护两个PCM设备的状态同步比如start、stop、pause这些操作都要同时作用到两个句柄上否则会出现一个在播一个停了的情况。另一个需要考虑的问题是采样率和格式的匹配。HDMI和Speaker可能支持不同的采样率如果上层给的采样率是48kHz而Speaker codec只支持44.1kHz就需要做重采样。不过RK3588的ES8388和HDMI控制器都支持48kHz所以这个问题在我的场景里不存在。如果你的硬件不支持可能需要在HAL里加一个简单的重采样模块或者强制上层使用统一的采样率。3.2 关键数据结构扩展先看tinyalsa_hal里output stream的结构体定义通常在audio_hw.h里。原生定义大概是这样struct stream_out { struct audio_stream_out stream; pthread_mutex_t lock; bool standby; struct audio_device *dev; struct pcm_config config; struct pcm *pcm; ... };我需要加一个额外的pcm句柄和对应的配置struct stream_out { struct audio_stream_out stream; pthread_mutex_t lock; bool standby; struct audio_device *dev; struct pcm_config config; struct pcm *pcm; // 主通路比如HDMI struct pcm *pcm_extra; // 额外通路比如Speaker struct pcm_config config_extra; bool extra_enabled; // 标记额外通路是否启用 ... };这里pcm用于HDMI输出pcm_extra用于Speaker输出。extra_enabled用来控制是否启用同步发声方便后续通过属性或调试接口动态开关。3.3 打开输出流的修改在start_output_stream或open_output_stream函数里需要根据设备类型判断是否要打开额外通路。核心逻辑如下static int start_output_stream(struct stream_out *out) { struct audio_device *adev out-dev; int ret 0; // 打开主通路 out-pcm pcm_open(out-dev-card, out-dev-device, PCM_OUT | PCM_MONOTONIC, out-config); if (!pcm_is_ready(out-pcm)) { ALOGE(cannot open pcm: %s, pcm_get_error(out-pcm)); return -ENODEV; } // 如果当前设备是HDMI额外打开Speaker通路 if (out-dev-out_device AUDIO_DEVICE_OUT_HDMI) { out-pcm_extra pcm_open(SOUND_CARD_SPEAKER, PCM_DEVICE_SPEAKER, PCM_OUT | PCM_MONOTONIC, out-config_extra); if (pcm_is_ready(out-pcm_extra)) { out-extra_enabled true; ALOGD(extra speaker pcm opened for HDMI sync); } else { ALOGE(cannot open extra pcm: %s, pcm_get_error(out-pcm_extra)); out-extra_enabled false; } } return ret; }这里的SOUND_CARD_SPEAKER和PCM_DEVICE_SPEAKER需要根据实际硬件定义我板子上是0和0。out-config_extra的采样率、通道数、格式要和主通路保持一致否则写入的数据会不匹配。实操心得pcm_open的时候一定要加PCM_MONOTONIC标志这样两个PCM设备的时间戳基准是一致的能减少音频不同步的问题。如果不加HDMI和Speaker之间可能会有几十毫秒的延迟差听起来像回声。3.4 写入函数的双通路处理out_write是音频数据真正写入PCM设备的地方原生代码只写一个句柄现在要改成同时写两个static ssize_t out_write(struct audio_stream_out *stream, const void *buffer, size_t bytes) { struct stream_out *out (struct stream_out *)stream; int ret 0; pthread_mutex_lock(out-lock); if (out-standby) { ret start_output_stream(out); if (ret ! 0) { goto exit; } out-standby false; } // 写主通路 if (out-pcm) { ret pcm_write(out-pcm, buffer, bytes); if (ret ! 0) { ALOGE(pcm_write failed: %s, pcm_get_error(out-pcm)); } } // 写额外通路 if (out-extra_enabled out-pcm_extra) { int ret_extra pcm_write(out-pcm_extra, buffer, bytes); if (ret_extra ! 0) { ALOGE(extra pcm_write failed: %s, pcm_get_error(out-pcm_extra)); } } exit: pthread_mutex_unlock(out-lock); return bytes; }这里有个细节要注意pcm_write是阻塞式的如果HDMI通路的缓冲区满了它会阻塞等待这期间Speaker通路也不会被写入。如果两个通路的消费速度差异很大可能会导致其中一个出现underrun。解决办法是把两个pcm_write放到不同的线程里或者用非阻塞模式加轮询。不过在实际测试中HDMI和Speaker的缓冲区深度和消费速率基本一致直接顺序写没有出现明显问题。3.5 停止与待机逻辑的同步out_standby和stop_output_stream里要同时关闭两个PCM句柄static int stop_output_stream(struct stream_out *out) { if (out-pcm) { pcm_close(out-pcm); out-pcm NULL; } if (out-pcm_extra) { pcm_close(out-pcm_extra); out-pcm_extra NULL; out-extra_enabled false; } return 0; }如果不做这个同步关闭会出现HDMI已经停了但Speaker还在响或者反过来导致下次打开时状态混乱。我一开始就踩了这个坑待机后重新播放Speaker通路没有重新打开结果只有HDMI出声。4. 编译、部署与调试验证4.1 编译环境的准备RK3588 Android 12的编译环境搭建这里不展开假设你已经能正常编译整个Android系统。tinyalsa_hal的源码通常在hardware/rockchip/audio/tinyalsa_hal/目录下编译产物是audio.primary.rk3588.so。单独编译这个模块可以用source build/envsetup.sh lunch rk3588_s-userdebug mmm hardware/rockchip/audio/tinyalsa_hal/编译完成后产物在out/target/product/rk3588_s/vendor/lib64/hw/audio.primary.rk3588.so64位系统。推送到板子上adb root adb remount adb push out/target/product/rk3588_s/vendor/lib64/hw/audio.primary.rk3588.so /vendor/lib64/hw/ adb shell sync adb reboot注意替换HAL库之后一定要重启因为audio HAL是在系统启动时加载的直接kill audioserver虽然能重新加载但有时候会有残留状态重启最干净。4.2 验证HDMI和喇叭是否同步发声重启后先确认HDMI插入时系统识别到的设备adb shell dumpsys audio | grep -A 5 Devices你应该能看到AUDIO_DEVICE_OUT_HDMI和AUDIO_DEVICE_OUT_SPEAKER同时出现在输出设备列表里。然后播放一个测试音频adb shell tinyplay /sdcard/test.wav -D 0 -d 0如果一切正常HDMI显示器上的音箱和板载喇叭应该同时出声。你可以用手分别捂住喇叭和HDMI音箱来确认两路都在工作。更精确的验证可以用tinycap同时录制两路输出或者用示波器测量两路模拟输出的波形看延迟差是否在可接受范围内。我实测下来HDMI和Speaker的延迟差在10ms以内人耳基本听不出回声。4.3 通过属性动态控制同步发声为了方便调试和后续产品化我加了一个系统属性来控制是否启用同步发声static bool is_extra_output_enabled() { char value[PROPERTY_VALUE_MAX]; property_get(persist.vendor.audio.hdmi_speaker_sync, value, 1); return (value[0] 1); }在start_output_stream里判断这个属性如果为0就不打开额外通路。这样在不需要同步发声的场景下可以通过setprop persist.vendor.audio.hdmi_speaker_sync 0关掉不用重新编译。4.4 常见问题排查表问题现象可能原因排查方法解决方案HDMI出声但喇叭无声额外通路未打开查看logcat中是否有extra speaker pcm opened检查out_device是否包含HDMI标志检查card/device编号喇叭出声但HDMI无声主通路打开失败pcm_is_ready返回false检查HDMI声卡是否被其他进程占用检查audio_policy_configuration.xml两路都有声但不同步时间戳基准不一致用示波器测量延迟差确保两个pcm_open都加了PCM_MONOTONIC播放一段时间后喇叭断流缓冲区underrun查看pcm_get_error输出增大config_extra.period_size和period_count待机后重新播放只有HDMI有声额外通路未重新打开检查standby标志和start_output_stream调用确保stop_output_stream里正确关闭并重置extra_enabled系统启动后第一次播放有杂音PCM设备初始化未完成检查codec上电时序在HAL初始化时提前打开一次PCM再关闭预热设备4.5 性能与延迟优化双通路写入会增加CPU占用因为同一份数据要写两次。在我的测试中48kHz、16bit、双声道的音频单通路写入时audioserver的CPU占用约3%双通路约5%增加不明显。但如果你的应用场景对功耗敏感可以考虑以下优化使用pcm_writei的mmap模式减少数据拷贝把两个pcm_write放到独立线程避免相互阻塞如果Speaker和HDMI的采样率不同在HAL层做一次重采样而不是让上层输出两份不同格式的数据实操心得RK3588的HDMI音频控制器和I2S控制器是独立的硬件模块它们可以真正并行工作不存在硬件层面的互斥。所以双通路同时发声在硬件上是完全可行的瓶颈只在软件层的策略和HAL实现。5. 扩展场景与兼容性考量5.1 双HDMI同时输出的处理我板子上有两路HDMI如果两路都插上系统会识别到两个HDMI设备。Android原生的策略是只选一个HDMI作为输出通常是最后插入的那个。如果要实现双HDMI加喇叭三路同时发声思路是一样的在HAL层打开三个PCM句柄out_write里写三次。不过三路同时写对CPU和内存带宽的压力更大而且三路之间的延迟同步更难保证。如果产品确实需要这个功能建议在硬件上选择支持多路音频同步输出的HDMI splitter或者在HAL层用时间戳对齐的方式做精细同步。5.2 与Android音频策略的兼容修改HAL层虽然绕开了framework的改动但有一个潜在问题AudioPolicyManager仍然认为HDMI和Speaker是互斥的它可能会在某些场景下主动把Speaker静音。比如系统收到电话或通知时可能会强制切到Speaker这时候HDMI的输出会被打断。要彻底解决这个问题还是需要改audio_policy_configuration.xml把HDMI和Speaker定义在同一个mixPort的devicePorts里让策略层知道这两个设备可以同时使用。这个文件在/vendor/etc/目录下修改后重启audioserver即可生效不需要重新编译framework。mixPort namehdmi_speaker_sync rolesource flagsAUDIO_OUTPUT_FLAG_PRIMARY profile name formatAUDIO_FORMAT_PCM_16_BIT samplingRates48000 channelMasksAUDIO_CHANNEL_OUT_STEREO/ devicePort tagNameHDMI Out typeAUDIO_DEVICE_OUT_HDMI rolesink/ devicePort tagNameSpeaker typeAUDIO_DEVICE_OUT_SPEAKER rolesink/ /mixPort5.3 不同Android版本的差异Android 12的audio HAL接口是hardware/audio.h的5.0或6.0版本和Android 11、13有一些差异。如果你移植到其他版本主要注意以下几点Android 13开始推荐使用AIDL版本的audio HALtinyalsa_hal的接口会有变化Android 11及以前audio_policy_configuration.xml的路径可能在/system/etc/而不是/vendor/etc/不同版本的pcm_config结构体字段可能有增减编译时注意对齐5.4 实际产品中的经验总结我在这个项目上前后折腾了大概两周大部分时间花在调试不同步和断流问题上。最后跑通的方案其实不复杂核心就是三个点HAL层多打开一个PCM、写入时多写一份、状态管理时多同步一个句柄。有几个经验值得分享第一不要一上来就改framework先从HAL层入手改动小、风险低、验证快第二PCM_MONOTONIC标志一定要加这是保证两路同步的关键第三调试的时候用tinyplay直接播放本地文件比通过应用层播放更容易定位问题因为排除了AudioTrack和AudioFlinger的干扰。后续如果要做更精细的同步可以考虑在HAL层引入一个简单的环形缓冲区把两路PCM的写入时间戳对齐到同一个时钟基准。不过对于大部分商显和会议场景当前的方案已经足够用了。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

手把手教你用MCP C# SDK开发MCP Server + Client,零基础也能轻松上手! 2026/9/28 19:50:53

手把手教你用MCP C# SDK开发MCP Server + Client,零基础也能轻松上手!

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
葡萄成熟度检测数据集:VOC+YOLO双格式1123张 2026/9/28 19:50:53

葡萄成熟度检测数据集:VOC+YOLO双格式1123张

简介:本资源是一个面向计算机视觉初学者与农业AI应用开发者的葡萄成熟度检测专用数据集,聚焦果实分级识别任务,适用于目标检测模型训练与算法验证。数据集共2000个文件,包含1123张高质量葡萄图像(JPG)、112…

阅读更多 →
GLM技术复盘:从论文到配置,TaoToken统一Key接入智谱模型家族 2026/9/28 19:50:37

GLM技术复盘:从论文到配置,TaoToken统一Key接入智谱模型家族

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
设备抖动别只怪驱动器:电机驱动系统抖动定位与解决 2026/9/28 19:50:37

设备抖动别只怪驱动器:电机驱动系统抖动定位与解决

设备一开机就抖、高速运行抖、低速抖、停下来还抖——这几个"抖"根本不是一回事。我接过不少现场电话,客户第一句话都是"电机抖了,驱动是不是有问题",可等我提着示波器到现场,十个里有八个问题根本不在驱动器…

阅读更多 →
2026年9月南京别墅庭院养护售后真能24小时响应吗 2026/9/28 19:50:30

2026年9月南京别墅庭院养护售后真能24小时响应吗

2026年的初秋,南京的梅雨季刚过,气温还没完全稳住。一位在河西拥有一栋联排别墅的私宅业主老陈,在凌晨两点给自家锦鲤池拍了张照片——水面上浮着一层油膜,几尾锦鲤无精打采地浮在水面。他没敢等天亮,直接拨通了一个电…

阅读更多 →
MCP 工程实践避坑指南:用 TaoToken 统一 Key 打通 JSON-RPC 与 Streamable HTTP 的 7 个生产落地细节 2026/9/28 19:50:30

MCP 工程实践避坑指南:用 TaoToken 统一 Key 打通 JSON-RPC 与 Streamable HTTP 的 7 个生产落地细节

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉