OpenHarmony播放音乐start请求从audio_host到ALSA库的调用流程04

1)文章由移远通信技术股份有限公司提供

2)以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改/删除

文章目录

  • 一、背景与目标
    • [1.1 环境信息](#1.1 环境信息)
    • [1.2 前置知识回顾](#1.2 前置知识回顾)
    • [1.3 本文追踪目标](#1.3 本文追踪目标)
  • 二、建议断点
  • [三、注意 SerStubStart 和 RenderStartImpl 有多个位置](#三、注意 SerStubStart 和 RenderStartImpl 有多个位置)
  • [四、audio_host 入口:SerStubStart](#四、audio_host 入口:SerStubStart)
  • [五、在 GDB 中反推 code=33 的枚举含义](#五、在 GDB 中反推 code=33 的枚举含义)
  • [六、SerStubStart 调用 AudioRenderStart](#六、SerStubStart 调用 AudioRenderStart)
  • [七、audio_host 内部调用到 RK3576 RenderStartImpl](#七、audio_host 内部调用到 RK3576 RenderStartImpl)
  • [八、AudioRenderStart 到 AudioOutputRenderStart](#八、AudioRenderStart 到 AudioOutputRenderStart)
  • [九、AudioOutputRenderStart 调用 renderIns->Start](#九、AudioOutputRenderStart 调用 renderIns->Start)
  • [十、RK3576 RenderStartImpl 做什么](#十、RK3576 RenderStartImpl 做什么)
  • [十一、串到 ALSA 库:snd_ctl_elem_write](#十一、串到 ALSA 库:snd_ctl_elem_write)
  • [十二、SndElementWriteInt 如何调用 ALSA control](#十二、SndElementWriteInt 如何调用 ALSA control)
  • [十三、SndElementWriteSwitch 也会进入 snd_ctl_elem_write](#十三、SndElementWriteSwitch 也会进入 snd_ctl_elem_write)
  • [十四、Start 路径不是 PCM 写数据路径](#十四、Start 路径不是 PCM 写数据路径)
  • 十五、结论

一、背景与目标

1.1 环境信息

  • 硬件平台: RK3576
  • 内核版本: Linux 6.6
  • OpenHarmony版本: 6.1.0.31 (API 23)
c 复制代码
// build/version.gni 关键配置
declare_args() {
  sdk_version = "6.1.0.31"
  api_version = "23"
  release_type = "Release"
  meta_version = "3.0.0"
  platform_version = "4.0.0"
}

1.2 前置知识回顾

前面的文章已经证明,播放 Start 请求已经从 media_service

下发到 audio_server,再通过 HDI proxy 到达 audio_host

c 复制代码
// 前文已证明的链路
media_service
  -> audio_server
  -> AudioRenderProxyCall(id=33)
  -> audio_host

audio_host 是用户态音频 HDI service 所在进程,负责处理来自 audio_server 的音频硬件操作请求。本文将继续分析最后一段关键路径:audio_host 收到 CMD_AUDIO_RENDER_START 后,如何通过 HDI service、ALSA adapter、RK3576 板级实现,最终调用到 ALSA 库。

1.3 本文追踪目标

本文聚焦于 audio_host 进程收到 CMD_AUDIO_RENDER_START 命令后的完整调用链路分析,涵盖以下关键路径:

  1. HDI Service层audio_host 中的 HDF IPC 请求处理
  2. ALSA Adapter层:OpenHarmony 音频适配器实现
  3. 板级驱动层:RK3576 特定的音频驱动实现
  4. ALSA库层:最终调用到 ALSA 用户态库

通过 GDB 调试验证以下完整调用链路:

c 复制代码
// 本文要证明的完整链路
audio_host
  HdfRemoteServiceStub::OnRemoteRequest(code=33)
    -> SerStubStart
    -> AudioRenderStart
    -> AudioOutputRenderStart
    -> RenderStartImpl
    -> SndElementWriteInt / SndElementWriteSwitch
    -> snd_ctl_elem_write

需要证明的两个核心问题:

  1. audio_host 收到的 code=33audio_server 发出的 CMD_AUDIO_RENDER_START 是同一个请求
  2. RK3576 的 Start 流程最终进入 ALSA control/mixer API,而非 PCM 数据写入路径

二、建议断点

attach audio_host 后:

gdb 复制代码
handle SIG38 nostop noprint pass

最小断点组合:

gdb 复制代码
b SerStubStart
b RenderStartImpl

这两个断点的作用:

text 复制代码
SerStubStart:
  证明 audio_host 收到了 audio_server 发来的 Start 请求。

RenderStartImpl:
  通过底层栈一次性补全 audio_host 内部调用关系。

为了把 ALSA 库也串上,再加一个 ALSA API 断点:

gdb 复制代码
b snd_ctl_elem_write

如果符号还没有加载:

gdb 复制代码
set breakpoint pending on
b snd_ctl_elem_write

三、注意 SerStubStart 和 RenderStartImpl 有多个位置

实际下断点时,GDB 可能显示多个位置:

text 复制代码
Breakpoint 1 at 0x7f27f0fe5c: SerStubStart. (2 locations)
1.1 audio_capture_stub.c:1069
1.2 audio_render_stub.c:1331

Breakpoint 2 at 0x7f27cccf40: RenderStartImpl. (2 locations)
2.1 alsa_snd_render.c:778
2.2 device/board/rockchip/rk3576/audio_alsa/vendor_render.c:165

原因是 capture 和 render 生成代码里都有 SerStubStart

通用 ALSA adapter 和 RK3576 板级代码里也都有 RenderStartImpl

本文关注播放链路,所以关键位置是:

text 复制代码
gen/drivers/interface/audio/v6_0/audio_render_stub.c:1331
device/board/rockchip/rk3576/audio_alsa/vendor_render.c:165

如果想减少干扰,可以删除不关心的位置:

gdb 复制代码
delete 1.1
delete 2.1

也可以保留多个位置,命中后通过文件路径判断当前是不是 render 路径。

四、audio_host 入口:SerStubStart

命中 SerStubStart

c 复制代码
Thread 2 "OS_IPC_0_4996" hit Breakpoint 1,
SerStubStart(serviceImpl=0x7fa968a830, audioRenderData=<optimized out>,
audioRenderReply=<optimized out>)
at gen/drivers/interface/audio/v6_0/audio_render_stub.c:1331

GDB 栈:

c 复制代码
#0  SerStubStart
    at gen/drivers/interface/audio/v6_0/audio_render_stub.c:1331
#1  HdfRemoteServiceStub::OnRemoteRequest
    (this=0x7fa96607c0, code=33, data=..., reply=..., option=...)
    at drivers/hdf_core/adapter/uhdf2/ipc/src/hdf_remote_adapter.cpp:60
#2  OHOS::IPCObjectStub::SendRequestInner
    (this=0x7fa96607c0, code=33, data=..., reply=..., option=...)
    at ipc_object_stub.cpp:409
#3  OHOS::BinderInvoker::GeneralServiceSendRequest
#4  OHOS::BinderInvoker::TargetStubSendRequest
#5  OHOS::BinderInvoker::Transaction

这里的关键是:

c 复制代码
HdfRemoteServiceStub::OnRemoteRequest(code=33)
  -> SerStubStart

这正好对应上一篇 audio_server 侧的:

c 复制代码
AudioRenderProxyCall(id=33)
  -> HdfRemoteAdapterOptionalDispatch(code=33)
  -> remote->SendRequest(33, ...)

五、在 GDB 中反推 code=33 的枚举含义

audio_host 的 GDB 中,可以直接反推:

gdb 复制代码
p (typeof(CMD_AUDIO_RENDER_GET_LATENCY))33

实测:

text 复制代码
$1 = CMD_AUDIO_RENDER_START

如果想使用当前栈里的变量,而不是手写 33

gdb 复制代码
frame 1
p code
p (typeof(CMD_AUDIO_RENDER_GET_LATENCY))code

预期:

text 复制代码
$ = 33
$ = CMD_AUDIO_RENDER_START

这里使用 typeof(CMD_AUDIO_RENDER_GET_LATENCY),是因为 CMD_AUDIO_RENDER_* 来自匿名 enum。没有一个稳定可用的枚举类型名时,可以借同组任意枚举常量的类型,把整数还原成枚举项。

因此,audio_host 入口可以写成:

text 复制代码
audio_host 收到 HDF IPC 请求:
code=33 == CMD_AUDIO_RENDER_START
随后分发到 audio_render_stub.c 的 SerStubStart。

六、SerStubStart 调用 AudioRenderStart

SerStubStart 生成代码:

c 复制代码
static int32_t SerStubStart(struct IAudioRender *serviceImpl,
    struct HdfSBuf *audioRenderData, struct HdfSBuf *audioRenderReply)
{
    int32_t audioRenderRet = HDF_FAILURE;
    if (serviceImpl == NULL) {
        audioRenderRet = HDF_ERR_INVALID_OBJECT;
        goto FINISHED;
    }

    if (serviceImpl->Start == NULL) {
        audioRenderRet = HDF_ERR_NOT_SUPPORT;
        goto FINISHED;
    }

    audioRenderRet = serviceImpl->Start(serviceImpl);
    ...
}

也就是说,SerStubStart 只是 HDI stub 层,它会继续调用 IAudioRender 服务端对象上的函数指针:

c 复制代码
serviceImpl->Start(serviceImpl)

从后续栈可知,该函数指针进入:

c 复制代码
AudioRenderStart

七、audio_host 内部调用到 RK3576 RenderStartImpl

继续执行,命中 RenderStartImpl

c 复制代码
Thread 2 "OS_IPC_0_4996" hit Breakpoint 2,
RenderStartImpl(renderIns=0x7fa9674040)
at device/board/rockchip/rk3576/audio_alsa/vendor_render.c:165

GDB 栈:

c 复制代码
#0  RenderStartImpl
    at device/board/rockchip/rk3576/audio_alsa/vendor_render.c:165
#1  AudioOutputRenderStart
    at drivers/peripheral/audio/supportlibs/alsa_adapter/src/alsa_lib_render.c:407
#2  AudioRenderStart
    at drivers/peripheral/audio/hdi_service/primary_impl/src/audio_render.c:107
#3  SerStubStart
    at gen/drivers/interface/audio/v6_0/audio_render_stub.c:1343
#4  HdfRemoteServiceStub::OnRemoteRequest(code=33)
    at drivers/hdf_core/adapter/uhdf2/ipc/src/hdf_remote_adapter.cpp:60
#5  OHOS::IPCObjectStub::SendRequestInner(code=33)
    at ipc_object_stub.cpp:409

这张栈已经把 audio_host 内部主体链路补齐:

c 复制代码
HdfRemoteServiceStub::OnRemoteRequest(code=33)
  -> SerStubStart
  -> AudioRenderStart
  -> AudioOutputRenderStart
  -> RenderStartImpl

八、AudioRenderStart 到 AudioOutputRenderStart

AudioRenderStart 位于:

text 复制代码
drivers/peripheral/audio/hdi_service/primary_impl/src/audio_render.c

从栈看:

c 复制代码
AudioRenderStart(handle=0x7fa968a830)
  -> AudioOutputRenderStart(...)

这一层属于 HDI service primary 实现。它把 IAudioRender::Start 这个 HDI 接口调用,

继续下发到 ALSA adapter 的 render start 操作。

九、AudioOutputRenderStart 调用 renderIns->Start

AudioOutputRenderStart 位于:

text 复制代码
drivers/peripheral/audio/supportlibs/alsa_adapter/src/alsa_lib_render.c

关键代码:

c 复制代码
int32_t AudioOutputRenderStart(
    const struct DevHandle *handle, int cmdId, const struct AudioHwRenderParam *handleData)
{
    int32_t ret;
    struct AlsaRender *renderIns = NULL;
    CHECK_NULL_PTR_RETURN_DEFAULT(handleData);

    renderIns = RenderGetInstance(handleData->renderMode.hwInfo.adapterName);
    CHECK_NULL_PTR_RETURN_DEFAULT(renderIns);

    ret = renderIns->Start(renderIns);
    if (ret != HDF_SUCCESS) {
        return ret;
    }

    return HDF_SUCCESS;
}

这里的关键是:

c 复制代码
renderIns->Start(renderIns)

对 RK3576 primary render 来说,这个函数指针被板级代码覆盖为:

text 复制代码
device/board/rockchip/rk3576/audio_alsa/vendor_render.c
RenderStartImpl

所以调用进入:

c 复制代码
AudioOutputRenderStart
  -> RenderStartImpl

十、RK3576 RenderStartImpl 做什么

RenderStartImpl 代码:

c 复制代码
static int32_t RenderStartImpl(struct AlsaRender *renderIns)
{
    int32_t ret;
    struct AlsaMixerCtlElement elem;
    struct AlsaSoundCard *cardIns = (struct AlsaSoundCard *)renderIns;
    SndElementItemInit(&elem);

    long volume = SND_DEFAULT_PLAYBACK_VOL;
    elem.numid = SND_NUMID_DACL_PLAYBACK_VOL;
    elem.name = SND_ELEM_DACL_PLAYBACK_VOL;
    ret = SndElementWriteInt(cardIns, &elem, volume);
    ...

    elem.numid = SND_NUMID_DACR_PLAYBACK_VOL;
    elem.name = SND_ELEM_DACR_PLAYBACK_VOL;
    ret = SndElementWriteInt(cardIns, &elem, volume);
    ...

    bool speakerOn = (renderIns->descPins != PIN_OUT_HEADSET);
    bool headphoneOn = (renderIns->descPins == PIN_OUT_HEADSET);

    elem.numid = SND_NUMID_SPEAKER_SWITCH;
    elem.name = SND_ELEM_SPEAKER_SWITCH;
    ret = SndElementWriteSwitch(cardIns, &elem, speakerOn);
    ...

    elem.numid = SND_NUMID_SPK_SWITCH;
    elem.name = SND_ELEM_SPK_SWITCH;
    ret = SndElementWriteSwitch(cardIns, &elem, speakerOn);
    ...

    elem.numid = SND_NUMID_HEADPHONE_SWITCH;
    elem.name = SND_ELEM_HEADPHONE_SWITCH;
    ret = SndElementWriteSwitch(cardIns, &elem, headphoneOn);
    ...

    elem.numid = SND_NUMID_HP_SWITCH;
    elem.name = SND_ELEM_HP_SWITCH;
    ret = SndElementWriteSwitch(cardIns, &elem, headphoneOn);
    ...

    return HDF_SUCCESS;
}

它主要做两类事情:

text 复制代码
1. 设置 DAC 左右声道播放音量:
   SND_NUMID_DACL_PLAYBACK_VOL
   SND_NUMID_DACR_PLAYBACK_VOL

2. 根据当前输出设备打开 speaker/headphone 相关 switch:
   SND_NUMID_SPEAKER_SWITCH
   SND_NUMID_SPK_SWITCH
   SND_NUMID_HEADPHONE_SWITCH
   SND_NUMID_HP_SWITCH

所以,RK3576 的 Start 阶段重点不是写 PCM 数据,而是配置 codec/mixer 输出通路。

十一、串到 ALSA 库:snd_ctl_elem_write

为了证明已经进入 ALSA 库,在 GDB 中对 snd_ctl_elem_write 下断点:

gdb 复制代码
b snd_ctl_elem_write
c
bt

实测命中:

c 复制代码
Thread 2 "OS_IPC_0_4996" hit Breakpoint 3,
snd_ctl_elem_write(ctl=0x7fa9669c30, data=0x7fa96fcc90)
at third_party/alsa-lib/src/control/control.c:1037

GDB 栈:

c 复制代码
#0  snd_ctl_elem_write
    at third_party/alsa-lib/src/control/control.c:1037
#1  SndElementWriteInt
    at drivers/peripheral/audio/supportlibs/alsa_adapter/src/alsa_soundcard.c:1117
#2  RenderStartImpl
    at device/board/rockchip/rk3576/audio_alsa/vendor_render.c:172
#3  AudioOutputRenderStart
    at drivers/peripheral/audio/supportlibs/alsa_adapter/src/alsa_lib_render.c:407
#4  AudioRenderStart
    at drivers/peripheral/audio/hdi_service/primary_impl/src/audio_render.c:107
#5  SerStubStart
    at gen/drivers/interface/audio/v6_0/audio_render_stub.c:1343
#6  HdfRemoteServiceStub::OnRemoteRequest(code=33)
    at drivers/hdf_core/adapter/uhdf2/ipc/src/hdf_remote_adapter.cpp:60
#7  OHOS::IPCObjectStub::SendRequestInner(code=33)
    at ipc_object_stub.cpp:409

这张栈把最终链路闭合:

c 复制代码
HdfRemoteServiceStub::OnRemoteRequest(code=33)
  -> SerStubStart
  -> AudioRenderStart
  -> AudioOutputRenderStart
  -> RenderStartImpl
  -> SndElementWriteInt
  -> snd_ctl_elem_write

其中 snd_ctl_elem_write 位于:

text 复制代码
third_party/alsa-lib/src/control/control.c

这就是 ALSA 用户态库的 control API。

十二、SndElementWriteInt 如何调用 ALSA control

SndElementWriteInt 位于:

text 复制代码
drivers/peripheral/audio/supportlibs/alsa_adapter/src/alsa_soundcard.c

关键代码:

c 复制代码
int32_t SndElementWriteInt(
    struct AlsaSoundCard *cardIns, const struct AlsaMixerCtlElement *ctlElem, long value)
{
    snd_ctl_t *alsaHandle = NULL;
    snd_ctl_elem_id_t *elem_id = NULL;
    snd_ctl_elem_info_t *elem_info = NULL;
    snd_ctl_elem_value_t *elem_value = NULL;

    int ret = snd_ctl_open(&alsaHandle, cardIns->ctrlName, 0);
    ...

    snd_ctl_elem_id_alloca(&elem_id);
    snd_ctl_elem_info_alloca(&elem_info);
    snd_ctl_elem_value_alloca(&elem_value);

    ret = SetElementInfo(alsaHandle, ctlElem, elem_info, elem_id);
    ...

    if (!snd_ctl_elem_info_is_writable(elem_info)) {
        return HDF_FAILURE;
    }

    snd_ctl_elem_value_set_id(elem_value, elem_id);
    snd_ctl_elem_type_t type = snd_ctl_elem_info_get_type(elem_info);
    if (type == SND_CTL_ELEM_TYPE_INTEGER) {
        snd_ctl_elem_value_set_integer(elem_value, 0, value);
    } else if (type == SND_CTL_ELEM_TYPE_INTEGER64) {
        snd_ctl_elem_value_set_integer64(elem_value, 0, (long long)value);
    } else {
        return HDF_FAILURE;
    }

    ret = snd_ctl_elem_write(alsaHandle, elem_value);
    ...
}

这说明 RenderStartImpl 中的:

text 复制代码
SndElementWriteInt(cardIns, &elem, volume)

最终转成 ALSA control 操作:

c 复制代码
snd_ctl_open
SetElementInfo
snd_ctl_elem_value_set_integer
snd_ctl_elem_write

十三、SndElementWriteSwitch 也会进入 snd_ctl_elem_write

RenderStartImpl 后面还有多次:

c 复制代码
SndElementWriteSwitch(cardIns, &elem, speakerOn);
SndElementWriteSwitch(cardIns, &elem, headphoneOn);

SndElementWriteSwitch 的结构和 SndElementWriteInt 类似,只是写入的是 boolean 类型:

c 复制代码
if (type == SND_CTL_ELEM_TYPE_BOOLEAN) {
    int value = on ? 1 : 0;
    snd_ctl_elem_value_set_boolean(elem_value, 0, value);
}

ret = snd_ctl_elem_write(alsaHandle, elem_value);

如果继续执行:

gdb 复制代码
c
bt

后续还会再次命中 snd_ctl_elem_write,上层会变成:

c 复制代码
#0 snd_ctl_elem_write
#1 SndElementWriteSwitch
#2 RenderStartImpl at vendor_render.c:190/198/206/214

这证明 speaker/headphone switch 同样是通过 ALSA control API 写入的。

十四、Start 路径不是 PCM 写数据路径

这里需要特别说明:Start 这条路径串到的是 ALSA control/mixer,不是 ALSA PCM 写数据。

也就是说,本文证明的是:

c 复制代码
CMD_AUDIO_RENDER_START
  -> 打开或配置 RK3576 播放输出通路
  -> 调用 snd_ctl_elem_write 写 mixer/control

而不是:

c 复制代码
CMD_AUDIO_RENDER_START
  -> snd_pcm_writei

真正把音频 PCM 数据写进 ALSA 的路径属于 Write / RenderFrame 流程,通常会走:

c 复制代码
IAudioRender::RenderFrame
  -> AudioRenderRenderFrame
  -> AudioOutputRenderWrite
  -> RenderWriteImpl
  -> RenderWritei
  -> snd_pcm_writei
text 复制代码
audio_host 的 Start 请求已经进入 ALSA 用户态库,并对 codec/mixer 控件进行了配置。

十五、结论

链路可以整理为:

c 复制代码
audio_host:
HdfRemoteServiceStub::OnRemoteRequest(code=33)
  -> SerStubStart
  -> serviceImpl->Start(serviceImpl)
  -> AudioRenderStart
  -> AudioOutputRenderStart
  -> renderIns->Start(renderIns)
  -> RenderStartImpl
  -> SndElementWriteInt
  -> snd_ctl_elem_write

其中:

c 复制代码
code=33 == CMD_AUDIO_RENDER_START

对 RK3576 来说,RenderStartImpl 做的是:

text 复制代码
1. 设置 DAC 左右声道播放音量。
2. 根据输出设备打开 speaker/headphone switch。
3. 通过 SndElementWriteInt / SndElementWriteSwitch 写 ALSA mixer/control。
4. 最终进入 alsa-lib 的 snd_ctl_elem_write。

完整四篇文章合起来,Start 请求从 app 到底层 ALSA control/mixer 的完整链路就是:

c 复制代码
app:
MiniMusic.hap
  -> Index.ets 点击 Play
  -> AVPlayerNapi::JsPlay
  -> AVPlayerNapi::PlayTask
  -> PlayerImpl::Play
  -> PlayerServiceProxy::Play
  -> SendRequest(PLAY)

media_service:
PlayerServiceStub::Play
  -> PlayerServer::Play
  -> PlayerServer::OnPlay
  -> PlayerServer::HandlePlay
  -> HiPlayerImpl::Play
  -> pipeline_->Start()
  -> Pipeline::Start()
  -> Filter::Start()
  -> AudioSinkFilter::DoStart
  -> AudioSink::Start
  -> AudioServerSinkPlugin::Start
  -> AudioRendererPrivate::Start
  -> RendererInClientInner::StartAudioStream
  -> IpcStreamProxy::Start
  -> SendRequest(IIpcStreamIpcCode::COMMAND_START)

audio_server:
IpcStreamStub::OnRemoteRequest(COMMAND_START)
  -> RendererInServer::Start
  -> HpaeRendererStreamImpl::Start
  -> HPAE::HpaeRendererManager::Start
  -> AudioRenderSink::Start
  -> AudioRenderProxyStart
  -> AudioRenderProxyCall(CMD_AUDIO_RENDER_START)

audio_host:
HdfRemoteServiceStub::OnRemoteRequest(CMD_AUDIO_RENDER_START)
  -> SerStubStart
  -> AudioRenderStart
  -> AudioOutputRenderStart
  -> RenderStartImpl
  -> SndElementWriteInt / SndElementWriteSwitch
  -> snd_ctl_elem_write

这就是 OpenHarmony 6.1 在 RK3576 上,播放 Start 请求从 app 一路到 ALSA control/mixer 的完整实测链路。

相关推荐
一颗小行星!7 天前
在线音频分轨网站推荐:5 款人声、伴奏与乐器分离工具对比
audio
山顶夕景1 个月前
【Audio】Audio encoder相关Benchmark
大模型·音视频·多模态·audio·检索·全模态
CheungChunChiu3 个月前
Linux 音频子系统完整梳理:ALSA、ASoC、DAPM、Codec、Machine、es8389 与 rk‑multicodecs 全解析
linux·运维·音视频·codec·audio·asla·dapm
summerkissyou19874 个月前
Android-Audio-根据音频焦点控制播放
android·audio
ameyume4 个月前
基于原生Android 16设置音量调用流程
android·audio
千里马学框架5 个月前
干货分享:车载音频audio调试开发之dumpsys CarAudioService剖析
android·音视频·面试题·audio·系统开发·车载audio·framework工程师
summerkissyou19875 个月前
Android-Audio-编码和解码
android·audio
奔跑吧 android5 个月前
【车载audio】【CarAudioService 05】【车载 Android 系统调试深度指南:解析 dumpsys car_service】
android·audio·audioflinger·aosp15·车载音频·车载audio·car_service
summerkissyou19876 个月前
Android-audio-常见面试题
android·audio