点击下方👇关注Android系统攻城狮
第197篇原创文章
每日充电:OS+MultiMedia学习之旅
01


先从全局链路看,应用启动后,先读取音频数据,再创建pa_mainloop和pa_context,随后发起与 PulseAudio 服务端的连接。当连接进入PA_CONTEXT_READY,context_cb()回调实现被触发,在回调中创建pa_stream、注册write_cb()并连接播放流。之后服务端在“需要数据”的时机触发write_cb(),应用再把PCM分片写入,直到全部发送完成。

💡Tips:PulseAudio播放不是APP直接写入ALSA,而是由主链路完成:APP -> libpulse -> PulseAudio Server -> sink_input -> sink -> module-alsa-sink -> ALSA
02


核心的步骤需要内部复杂的注册、通知、消息等待,真正的连接完成、状态变化、建流、写数据、播放结束,都不是同步顺序执行,而是等mainloop跑起来之后,由事件循环一步步推进完成。

💡Tips:pa_context_set_state_callback() 只是注册,不是执行。只有连接完成并进入 PA_CONTEXT_READY后,context_cb回调实现才会真正被触发。
03


从函数调用角度,这条链路可以拆成四段来看。
第一阶段是创建 mainloop、创建 context、注册回调、发起连接。
第二阶段是mainloop驱动连接过程,直到context进入READY。
第三阶段是在context_cb()中创建pa_stream,注册write_cb(),并连接playback。
第四阶段是write_cb()被不断触发,应用通过pa_stream_write()分片提交PCM;当全部数据写完后,再调用pa_stream_drain(),最后由drain_cb()退出主循环。


💡Tip:pa_mainloop_run()不是一次执行完就返回的普通函数。它内部会持续迭代,反复执行循环:prepare -> ppoll -> dispatch。
04


应用真正写出去的数据,当write_cb()被触发时,PulseAudio会告诉应用这次最多还能写多少。应用再计算当前还剩多少数据,把这一段通过pa_stream_write()交给 libpulse。每写完一次,data_pos都向后推进,直到全部数据写完。

💡Tips:data是PCM缓冲区,data_len是总长度,data_pos是当前发送游标。所以write_cb()的本质,就是按服务端请求节奏分批喂数据。
05


从模块关系上看,应用层只直接调用 libpulse,但真正的数据处理并不止停留在客户端。客户端通过 native protocol 把请求和音频数据传给 PulseAudio Server,服务端收到后创建对应的sink_input,把这一路播放流挂接到目标sink上。之后 sink负责调度、缓存、混音与输出,真正往下写硬件时,通常由module-alsa-sink把数据交给ALSA 设备。

💡Tips:sink_input是理解播放链路的关键,它可以简单理解成:服务端为这一路播放流创建的输入对象。
06


要真正看懂这条链路,几个核心对象必须建立清晰分工。
1.pa_mainloop负责事件等待与分发。
2.pa_context负责客户端连接状态。
3.pa_stream负责播放流本身,记录采样参数、状态和写回调。
4.context_cb()、write_cb()、drain_cb()则是应用参与这套异步流程的三个关键入口。
5.可以说,mainloop负责跑流程,context负责连服务,stream负责承载音频流,回调负责在正确时机做正确的事。

💡 Tips:mainloop管事件,context管连接,stream管流,callback管动作。
07


PulseAudio这套播放流程简单,但是要搞明白它背后的播放机制的话,还是需要花点时间来研究下。
本篇内容的重点,不是把PulseAudio播放链路一次性吃透,而是先把主链路打通,只要这条链路上理解了,那么这条线上的所串联的模块,我们就能更容易的掌握它。
若读者朋友发现有错误、疑问的地方,或者好的建议,欢迎拍砖!!!
精彩文章合集
专栏推荐