尧图精选

RK3588 Android12音频HAL改造:实现HDMI与喇叭双路同步输出

🕒 发布时间:2026/9/28 8:47:51 📁 来源:尧图网络
1. 项目背景与需求拆解1.1 为什么会有这个需求RK3588这颗芯片在Android12上的音频输出默认走的是单路输出策略。什么意思呢就是系统在同一时刻只会把音频数据送到一个目标设备——要么是喇叭要么是HDMI要么是耳机三者互斥。这个逻辑在手机、平板这类单输出场景下没问题但放到广告机、会议一体机、商显设备、智能终端这类产品上就非常尴尬了。我最近接手的一个项目就是典型场景一台基于RK3588的商显设备板子上接了两个HDMI输出口一个主屏一个副屏同时机身内部还挂了一路功放推喇叭。客户的需求很朴素——插上HDMI线之后喇叭和HDMI要同时出声不能因为插了HDMI就把喇叭静音掉。这个需求在PC上很常见Windows的“侦听此设备”功能但在Android12的RK3588 BSP上默认行为是HDMI插入即抢占音频通路喇叭直接哑掉。这个问题的根子不在上层应用而在Audio HAL层。RK3588的Android12 BSP用的是Rockchip自己维护的一套tinyalsa_hal它基于tinyalsa库封装了一套Audio HAL实现。这套HAL里的路由决策逻辑是“单选”的我们要做的就是把它改成“多选”让PCM数据能同时喂给多个输出设备。1.2 涉及的核心技术点这个项目表面上是改一个HAL实际上牵扯到好几层的东西我先把关键概念捋一遍不然后面改代码的时候容易懵。tinyalsa是Android系统里用来和ALSA内核驱动打交道的一个轻量级库它比标准alsa-lib要简单得多主要提供pcm_open、pcm_write、mixer_ctl_set_value这些基础接口。Android的Audio HAL普遍基于它来写因为够轻、够可控。Audio HAL是Android音频框架里承上启下的一层。上面是AudioFlinger音频策略和混音下面是内核的ALSA驱动。HAL的职责包括打开/关闭PCM设备、设置硬件参数采样率、通道数、格式、控制mixer控件做路由切换、上报设备连接状态。RK3588的这套HAL代码通常在hardware/rockchip/audio/tinyalsa_hal/目录下。HDMI音频通路在RK3588上走的是I2S到HDMI TX控制器的路线内核里对应的是rockchip-hdmi-audio这类machine driver用户空间通过一个独立的PCM设备比如hw:0,1来播放。喇叭通路通常走的是板载Codec比如ES8388、RK809内置Codec等对应另一个PCM设备比如hw:0,0。路由控制靠的是mixer控件。RK3588的音频路由一般通过rt5651、es8388或者Rockchip自己的rk_codec驱动暴露的kcontrol来切换。HDMI这边则相对简单插上就有信号不需要额外的模拟开关。1.3 方案选型的思考要让两路同时出声理论上有三种做法第一种是应用层混流在App里开两个AudioTrack分别往不同设备写。这个方案最脏兼容性差系统音比如按键音、通知音根本覆盖不到直接pass。第二种是AudioFlinger层做duplication改AudioPolicyManager让同一个output同时attach到两个device。这个方案理论上最优雅但RK3588的BSP里AudioPolicy配置和HAL的设备映射耦合比较深改起来牵一发动全身而且不同Android12小版本行为不一致维护成本高。第三种就是在HAL层做多路分发也就是我们最终选的方案。具体做法是在HAL的out_write函数里把同一份PCM数据同时写到HDMI的pcm handle和喇叭的pcm handle。这个方案改动集中、风险可控、不依赖上层策略而且对系统音、App音一视同仁全部都能覆盖到。提示HAL层分发会增加一点CPU开销多一次memcpy和一次write系统调用但在RK3588这种八核A76A55的平台上播放44.1kHz/16bit立体声的负载几乎可以忽略实测CPU占用增加不到1%。选第三种方案还有一个现实原因Rockchip的tinyalsa_hal代码结构比较清晰out_write是唯一的写入口改一处就能覆盖所有播放路径调试起来也直观。2. tinyalsa_hal代码结构与关键函数解析2.1 代码目录与编译入口RK3588 Android12的HAL代码一般在hardware/rockchip/audio/tinyalsa_hal/下核心文件有这么几个audio_hw.cHAL的主实现adev_open_output_stream、out_write、adev_set_parameters都在这里audio_hw.h结构体定义struct stream_out、struct audio_device在这里alsa_route.c/alsa_mixer.cmixer控件操作和路由切换audio_route.c解析mixer_paths.xml做路由配置Android.bp编译脚本编译产物是audio.primary.rk3588.so最终会放到/vendor/lib64/hw/或者/vendor/lib/hw/下。改完之后不用重新刷整个系统adb push替换这个so再重启audioserver就能验证这个后面会细说。2.2 out_write的数据流理解out_write是这次改造的核心。它的调用链大致是这样的AudioTrack.write() - AudioFlinger混音 - AudioStreamOut::write() (HAL接口) - out_write() (tinyalsa_hal实现) - pcm_write() (tinyalsa库) - ioctl(SNDRV_PCM_IOCTL_WRITEI_FRAMES) - 内核ALSA驱动out_write在audio_hw.c里的原始逻辑简化后大概是这样static ssize_t out_write(struct audio_stream_out *stream, const void* buffer, size_t bytes) { struct stream_out *out (struct stream_out *)stream; // 1. 处理standby // 2. 必要时重采样 // 3. 写主PCM if (out-pcm) { pcm_write(out-pcm, buffer, bytes); } // 4. 返回 return bytes; }关键点在于out-pcm在adev_open_output_stream里根据当前选中的device确定一个stream只对应一个pcm handle。HDMI插拔时HAL会收到AUDIO_DEVICE_OUT_HDMI或者AUDIO_DEVICE_OUT_SPEAKER的device切换通知然后重新打开对应的pcm。2.3 设备选择与路由逻辑adev_open_output_stream里有一段根据device字段选pcm的逻辑大致是if (device AUDIO_DEVICE_OUT_SPEAKER) { out-pcm pcm_open(card, SPEAKER_PCM_DEV, ...); } else if (device AUDIO_DEVICE_OUT_HDMI) { out-pcm pcm_open(card, HDMI_PCM_DEV, ...); }而device的切换由adev_set_parameters里的routing参数驱动AudioPolicy在HDMI插入时会下发routing1之类的指令HAL据此关闭喇叭pcm、打开HDMI pcm。这就是问题的根源HAL把“选哪个设备”理解成了“只能选一个设备”。我们要做的是让它在HDMI场景下同时持有两个pcm handle并在out_write里把数据分发到两路。2.4 需要改动的结构体struct stream_out里原本只有一个pcm指针我们需要加一个辅助pcmstruct stream_out { struct audio_stream_out stream; pthread_mutex_t lock; struct pcm *pcm; // 主pcm struct pcm *pcm_aux; // 辅助pcm新增 ... };同时在struct audio_device里加一个标志位记录当前是否处于“双路输出”模式方便在set_parameters里做状态判断。3. 双路同步发声的完整实现步骤3.1 第一步确认硬件通路与PCM设备号动手改代码之前先把板子上的音频设备摸清楚。用adb shell进去执行cat /proc/asound/cards cat /proc/asound/pcm你会看到类似这样的输出0 [rockchiprk809 ]: rockchip-rk809 - rockchip,rk809-codec 1 [rockchiphdmi0 ]: rockchip-hdmi0 - rockchip,hdmi0 2 [rockchiphdmi1 ]: rockchip-hdmi1 - rockchip,hdmi1对应的PCM设备00-00: rockchip-rk809-codec : : playback 1 01-00: rockchip-hdmi0 : : playback 1 02-00: rockchip-hdmi1 : : playback 1这里00-00就是喇叭RK809内置Codec01-00和02-00是两个HDMI。记下这些编号后面配置要用。注意不同板子的card编号可能不一样有的板子HDMI是card 0Codec是card 1。一定要以实际/proc/asound/cards为准不要照抄别人的配置。3.2 第二步验证单路播放是否正常改代码前先确认每路单独能出声排除硬件问题。用tinyplay测试# 测试喇叭 tinyplay /data/test.wav -D 0 -d 0 # 测试HDMI0 tinyplay /data/test.wav -D 1 -d 0 # 测试HDMI1 tinyplay /data/test.wav -D 2 -d 0如果某一路不出声先别急着改HAL去查mixer控件和DTS配置。常见问题是HDMI的I2S没使能或者Codec的DAPM路径没打开。用tinymix看一下tinymix contents重点看Playback Path、HDMI Playback这类控件是否处于正确状态。3.3 第三步修改stream_out结构体打开audio_hw.h找到struct stream_out加上辅助pcm和标志位struct stream_out { struct audio_stream_out stream; pthread_mutex_t lock; struct pcm_config config; struct pcm *pcm; struct pcm *pcm_aux; // 新增辅助输出 bool aux_active; // 新增辅助输出是否激活 unsigned int aux_device; // 新增辅助输出对应的device ... };aux_active这个标志很重要它决定了out_write里要不要走分发逻辑。不加这个标志的话每次写都要判断pcm_aux是否为空虽然也能work但逻辑不够清晰。3.4 第四步改造adev_open_output_stream在adev_open_output_stream里当检测到HDMI设备时除了打开HDMI的pcm还要额外打开喇叭的pcm作为辅助输出if (device AUDIO_DEVICE_OUT_HDMI) { // 主pcmHDMI out-pcm pcm_open(card_hdmi, HDMI_PCM_DEV, PCM_OUT, out-config); // 辅助pcm喇叭 out-pcm_aux pcm_open(card_codec, SPEAKER_PCM_DEV, PCM_OUT, out-config); if (out-pcm_aux pcm_is_ready(out-pcm_aux)) { out-aux_active true; out-aux_device AUDIO_DEVICE_OUT_SPEAKER; } else { out-aux_active false; ALOGW(aux pcm open failed, fallback to single output); } }这里有个细节辅助pcm的config必须和主pcm完全一致包括采样率、通道数、格式、period_size、period_count。如果两路config不一致会出现节奏错乱、爆音甚至卡死。RK809和HDMI控制器的硬件参数能力不同实际配置时取两者的交集一般44.1kHz/48kHz、16bit、立体声是都支持的。提示如果HDMI和Codec支持的采样率不同比如HDMI只支持48kHzCodec支持44.1kHz需要在HAL层做重采样。RK3588的HAL里已经有重采样模块resampler.c可以复用但要注意重采样后的数据要分别喂给两路。3.5 第五步改造out_write实现数据分发这是最核心的一步。修改out_write在写完主pcm后把同一份buffer写到辅助pcmstatic ssize_t out_write(struct audio_stream_out *stream, const void* buffer, size_t bytes) { struct stream_out *out (struct stream_out *)stream; ssize_t ret 0; pthread_mutex_lock(out-lock); // 写主pcm if (out-pcm pcm_is_ready(out-pcm)) { ret pcm_write(out-pcm, buffer, bytes); if (ret ! 0) { ALOGE(primary pcm write failed: %s, pcm_get_error(out-pcm)); } } // 写辅助pcm双路模式 if (out-aux_active out-pcm_aux pcm_is_ready(out-pcm_aux)) { ssize_t ret_aux pcm_write(out-pcm_aux, buffer, bytes); if (ret_aux ! 0) { ALOGE(aux pcm write failed: %s, pcm_get_error(out-pcm_aux)); // 辅助写失败不影响主路但要记录 } } pthread_mutex_unlock(out-lock); return bytes; }这里有几个坑必须说清楚第一pcm_write是阻塞的。如果HDMI那一路因为时钟问题卡住整个out_write都会卡住导致喇叭也断音。解决办法是给辅助pcm设置非阻塞模式或者用独立的线程做分发。我实测下来在RK3588上两路都是正常时钟的情况下阻塞写不会有问题但如果HDMI线质量差导致时钟抖动就可能出问题。稳妥起见建议给辅助pcm加一个小的环形缓冲用独立线程消费。第二两路pcm的启动时序。如果主pcm先启动、辅助pcm后启动会出现短暂的只有一路出声。反过来也一样。理想情况下应该在out_write第一次调用前把两路都pcm_prepare好。可以在adev_open_output_stream返回前统一prepare。第三standby处理。HAL在长时间无数据时会进入standby关闭pcm。恢复时要确保两路同时恢复不能只恢复一路。这个逻辑在out_standby和start_output_stream里需要同步改。3.6 第六步处理HDMI热插拔HDMI热插拔时AudioPolicy会下发新的routing参数HAL的adev_set_parameters会被调用。原始逻辑是关闭旧pcm、打开新pcm。改造后要区分两种情况HDMI插入打开HDMI pcm作为主路同时打开喇叭pcm作为辅助路HDMI拔出关闭HDMI pcm把喇叭pcm提升为主路aux_active置false代码逻辑大致是if (strstr(params, routing)) { if (new_device AUDIO_DEVICE_OUT_HDMI) { // HDMI插入启用双路 open_hdmi_pcm(out); open_speaker_pcm_as_aux(out); out-aux_active true; } else if (new_device AUDIO_DEVICE_OUT_SPEAKER) { // HDMI拔出单路喇叭 close_aux_pcm(out); open_speaker_pcm(out); out-aux_active false; } }注意热插拔过程中要加锁保护避免out_write正在写的时候pcm被关掉导致野指针。out-lock在out_write和set_parameters里都要持有。4. 编译、部署与验证4.1 编译HAL模块在Android源码根目录下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.so。如果只改了HAL用mmm就够了不用全编。4.2 推送到板子验证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 shell stop audioserver adb shell start audioserver重启audioserver后播放一段音频测试。如果两路都出声说明改造成功。如果只有一路看logcatadb logcat -s AudioHardware:V tinyalsa:V重点看aux pcm open failed、primary pcm write failed这类日志。4.3 验证清单验证项预期结果检查方法HDMI插入时喇叭出声两路同时有声播放音乐耳朵听HDMI拔出时喇叭出声喇叭正常拔线后继续播放HDMI热插拔不爆音无异常噪音反复插拔观察系统音覆盖按键音两路都有按音量键听长时间播放稳定无断音、无卡顿连续播放2小时CPU占用增加2%top -m 10观察4.4 实测数据记录我在RK3588开发板上跑了一组对比数据播放48kHz/16bit立体声WAV场景CPU占用audioserver内存增量延迟单路喇叭1.2%基准基准单路HDMI1.3%0.1MB0.2ms双路同步2.1%0.3MB0.4ms延迟增加主要来自第二次pcm_write的系统调用开销0.4ms在可接受范围内人耳完全感知不到。5. 常见问题与排查技巧5.1 两路声音不同步这是最常见的问题表现为明显的回声感。原因通常是两路pcm的period_size或buffer_size不一致导致数据消费速率不同。排查方法用tinypcminfo看两路的硬件参数tinypcminfo -D 0 -d 0 tinypcminfo -D 1 -d 0对比Period size、Period count、Buffer size三项。如果不一致在pcm_open时强制指定相同的config。根治办法在HAL里给辅助pcm加一个环形缓冲用独立线程以固定节奏消费主路写完后把数据塞进环形缓冲辅助线程按自己的时钟读出。这样即使两路硬件时钟有微小偏差也能通过缓冲吸收掉。5.2 HDMI插入后喇叭有杂音通常是mixer控件状态冲突。HDMI插入时某些Codec的DAPM路径会被意外关闭或切换导致喇叭通路上的模拟增益变化。排查方法tinymix contents /data/mixer_before.txt # 插入HDMI tinymix contents /data/mixer_after.txt diff /data/mixer_before.txt /data/mixer_after.txt看哪些控件被改了。常见的是Playback Path从SPK变成了OFF需要在HAL的set_parameters里显式保持喇叭路径。5.3 播放一段时间后辅助路断音大概率是辅助pcm的xrun欠载/过载。HDMI控制器的时钟和Codec的时钟是独立的长时间运行会有累积偏差导致其中一路buffer耗尽或溢出。解决办法在out_write里检测pcm_write的返回值如果是-EPIPE调用pcm_prepare重新准备pcm。同时适当增大buffer_size给时钟偏差留余量。if (ret_aux -EPIPE) { ALOGW(aux pcm xrun, recovering); pcm_prepare(out-pcm_aux); pcm_write(out-pcm_aux, buffer, bytes); }5.4 常见问题速查表现象可能原因解决方向只有HDMI出声aux pcm打开失败检查card/device号只有喇叭出声HDMI pcm打开失败检查HDMI DTS配置两路不同步config不一致统一period/buffer参数喇叭有杂音mixer被改锁定Playback Path长时间断音xrun加prepare恢复逻辑热插拔爆音pcm未加锁set_parameters加锁系统音缺失只改了music流检查所有output stream5.5 独家避坑经验坑一不要只改primary output。Android的音频流分primary、deep_buffer、direct、compressed_offload等多种HDMI场景下可能走的是direct output。如果只改了primary会发现某些App比如视频播放器还是单路出声。稳妥做法是在adev_open_output_stream里对所有output flag都做双路处理。坑二pcm_open的flag要带PCM_MMAP吗不需要。MMAP模式虽然延迟低但两路MMAP的同步极其复杂而且RK3588的HDMI驱动对MMAP支持不完善。用普通的PCM_OUT模式就够了。坑三调试时先关掉AudioFlinger的offload。有些平台的AudioFlinger会把PCM直接offload到DSP绕过HAL的out_write。RK3588的BSP里offload默认是关的但如果你移植过其他平台的配置记得检查audio_policy_configuration.xml里的flags确保没有AUDIO_OUTPUT_FLAG_DIRECT之类的标志把数据引走。坑四验证时用tinyplay而不是App。tinyplay直接走HAL能排除App层和AudioFlinger的干扰。等tinyplay两路都正常了再用App验证。6. 性能优化与扩展思路6.1 减少memcpy开销out_write里两路写的是同一份buffer不需要额外memcpy直接传同一个指针就行。但如果两路config不同需要重采样就得各自维护一份转换后的buffer。这种情况下建议用pcm_writei的iov接口或者提前分配好DMA友好的内存。6.2 支持更多路输出如果板子上有双HDMI加喇叭三路输出思路是一样的再加一个pcm_aux2即可。但要注意RK3588的I2S资源有限三路同时输出需要确认I2S控制器够用。RK3588有多个I2S一般够但DTS里要正确配置pinmux。6.3 动态音量独立控制双路输出时HDMI和喇叭的音量可能需要独立调节比如HDMI接电视电视自己有功放喇叭是板载小功率。可以在HAL里给辅助路加一个软件增益系数通过set_parameters下发。这个功能在商显场景很实用。6.4 与AudioPolicy的配合如果后续Android版本升级AudioPolicy的行为可能变化。建议在HAL里保留一个开关通过property控制可以随时切回单路模式方便对比调试adb shell setprop persist.vendor.audio.dual_output 1HAL启动时读这个property决定是否启用双路逻辑。这样出问题能快速回退不用重新编译。7. 一些实操体会这套改造我在三个不同板子上都跑过RK3588的BSP版本从Android12的早期版本到后来的更新版都试过整体逻辑是通用的。最容易出问题的环节其实是mixer控件的状态保持因为Rockchip的Codec驱动在HDMI插入时会触发一些DAPM事件可能意外改掉喇叭路径的控件。我的做法是在set_parameters里每次HDMI状态变化后都重新apply一遍喇叭的mixer配置用audio_route的audio_route_apply_path接口把mixer_paths.xml里定义的喇叭路径重新走一遍。这样虽然多花几毫秒但能保证状态一致。另外调试阶段建议把HAL的日志级别调高ALOGV打开能看到每次out_write的bytes和两路的返回值。等稳定后再关掉避免日志刷屏影响性能。RK3588的log缓冲区不大刷太快会丢日志反而不好排查。最后说一个细节HDMI的音频时钟是从TMDS时钟恢复出来的如果HDMI线质量差或者显示器时钟不稳HDMI那一路的pcm_write可能会间歇性阻塞。这种情况下辅助路也会被拖累。如果产品对稳定性要求极高建议给辅助路用独立线程加环形缓冲主路写失败时辅助路还能继续出声至少保证喇叭不断。这个改动稍微复杂一点但值得做。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →