ARTICLE · 1240728
AudioReach 源码分析(三):深入 PAL,Stream、Session、Device 是如何协作的?
系列第三篇。上一篇停在「Stream 建好、Session 打开后端」这一步,没有往下拆。这一篇进到 PAL 内部,把三类对象的关系讲清楚:Stream 管流、Session 管会话、Device 管设备,三者怎么分工、怎么协作,数据最后从哪个口子写出去。全文基于 SA8295P (Lemans) Android 13 Automotive 源码树,代码都标了文件名和函数名。
一、创建了 Stream,为什么还需要 Session
上一篇的结尾停在 pal_stream_open 建出 Stream 对象、StreamPCM::open 里打开后端。但一个 Stream 对象里同时挂了 session 指针和 mDevices 向量,这是为什么。拆成三个问题:
Stream 和 Session 为什么不是同一个对象? 真正写 PCM 数据的代码在谁手里? Device 在什么阶段参与,它不开 PCM 又管什么?
答案先摆出来:Stream 对应一条 Android 音频流,管状态机和对外接口;Session 对应一次后端会话,是唯一碰 ALSA PCM 或 AGM 会话的地方;Device 对应一个逻辑输出端点(扬声器、耳机、蓝牙),管路由使能和媒体配置,不碰数据。三者是 Stream 持有 Session 和 Device,生命周期层层委托。
而且不是所有流都走同一种 Session。COMPRESSED、VOICE_CALL、NON_TUNNEL 各自有专属 Session,其余(含车载播放的 PLAYBACK_BUS)落到 SessionAlsaPcm。
二、PAL 内部结构:先建立对象关系图

几个要点:
Stream 是基类,派生类有 StreamPCM、StreamCompress、StreamNonTunnel、StreamInCall、StreamSoundTrigger 等。 Stream 基类持有 Session* session(一个)和std::vector<std::shared_ptr<Device>> mDevices(多个)。一个流对应一个会话,可以同时路由到多个设备。Session 是基类,派生类有 SessionAlsaPcm、SessionAlsaCompress、SessionAlsaVoice、SessionAgm。 Device 是基类,按 pal_device_id 派生出 Speaker、Headphone、Handset、Bluetooth、DisplayPort、USB 等二十多个类。
三层的工厂方法分别是 Stream::create、Session::makeSession、Device::getInstance,都靠 switch 按类型分派。
三、Stream:音频流的组织层
Stream 基类(stream/inc/Stream.h)定义了流的状态机和对外接口。状态机是 stream_state_t:IDLE、INIT、OPENED、STARTED、PAUSED、SUSPENDED、STOPPED。接口是 open/close/start/stop/prepare/write/read/mute/pause/resume 这一套虚函数,外加一个静态 create 工厂。
create 是第一个分派点,在 stream/src/Stream.cpp,按 sAttr->type 决定子类:
switch (sAttr->type) {case PAL_STREAM_LOW_LATENCY:case PAL_STREAM_DEEP_BUFFER:case PAL_STREAM_GENERIC:case PAL_STREAM_VOIP_TX:case PAL_STREAM_VOIP_RX:case PAL_STREAM_PCM_OFFLOAD:case PAL_STREAM_VOICE_CALL:case PAL_STREAM_LOOPBACK:case PAL_STREAM_ULTRA_LOW_LATENCY:case PAL_STREAM_PROXY:case PAL_STREAM_PLAYBACK_BUS:case PAL_STREAM_HAPTICS:case PAL_STREAM_RAW: stream = newStreamPCM(...); // 十四种流都进这里break;case PAL_STREAM_COMPRESSED: stream = newStreamCompress(...);break;case PAL_STREAM_NON_TUNNEL: stream = newStreamNonTunnel(...);break; ...}车载常规播放(PLAYBACK_BUS)落在第一个 case,进 StreamPCM。十四种流共用一个子类,说明 StreamPCM 是 PCM 流的通用实现,覆盖低延迟、深缓冲、VOIP、环回、总线播放等大多数场景。
StreamPCM 的构造(stream/src/StreamPCM.cpp)里干了两件事,正好接上第一章的问题:
session = Session::makeSession(rm, sattr); // 建会话for (int i = 0; i < no_of_devices; i++) { dev = Device::getInstance(&dattr[i], rm); // 建设备 mDevices.push_back(dev);}Stream 建出来的时候,Session 和 Device 也跟着建好了,三者从构造期就绑在一起。
生命周期函数都在 StreamPCM 里做状态检查,然后委托出去。open 先开 Session 再开 Device:
if (currentState == STREAM_IDLE) { status = session->open(this); // 先会话for (i...) status = mDevices[i]->open(); // 再设备 currentState = STREAM_INIT;}start 的输出分支顺序相反,先起设备再起会话:
rm->lockGraph();for (i...) status = mDevices[i]->start(); // 先设备status = session->prepare(this);status = session->start(this); // 后会话rm->unlockGraph();stop 反过来,先停会话再停设备;close 先关会话再关设备。write 最直接,就是转给 session:
status = session->write(this, SHMEM_ENDPOINT, buf, &size, 0);Stream 不管 PCM 也不管路由,只把一次流的生命周期拆成对 Session 和对 Device 的调用,顺序和状态机在这里定。
四、Session:音频会话如何连接底层
Session 的工厂是 makeSession(session/src/Session.cpp):
switch (sAttr->type) {case PAL_STREAM_COMPRESSED: s = newSessionAlsaCompress(rm); break;case PAL_STREAM_VOICE_CALL: s = newSessionAlsaVoice(rm); break;case PAL_STREAM_NON_TUNNEL: s = newSessionAgm(rm); break;default: s = newSessionAlsaPcm(rm); break;}PLAYBACK_BUS 不在前三个 case,落 default,建 SessionAlsaPcm。SessionAlsaPcm 是绝大多数 PCM 流的落点。
SessionAlsaPcm::open(session/src/SessionAlsaPcm.cpp)做的是前端分配和前后端连接。它先 allocateFrontEndIds 拿到前端 PCM 设备号,getBackEndNames 拿到后端名,getVirtualAudioMixer 拿到虚拟声卡的 mixer,然后交给 SessionAlsaUtils::open,由后者通过 mixer 控制把前端连到后端:
pcmDevIds = rm->allocateFrontEndIds(sAttr, 0); // 分配前端号rm->getBackEndNames(associatedDevices, rxAifBackEnds, txAifBackEnds);rm->getVirtualAudioMixer(&mixer);status = SessionAlsaUtils::open(s, rm, pcmDevIds, rxAifBackEnds);SessionAlsaUtils::open 里的动作是 FE_CONTROL、FE_METADATA、FE_CONNECT 这几个 mixer 控制,把前端(PCM 流)和后端(通往 DSP 的 AIF/DAI)接上。
PCM 设备的打开在 start 里。SessionAlsaPcm::start 先用流的媒体配置填 pcm_config,然后 pcm_open:
config.rate = sAttr.out_media_config.sample_rate;config.format = SessionAlsaUtils::palToAlsaFormat(sAttr.out_media_config.aud_fmt_id);config.channels = sAttr.out_media_config.ch_info.channels;...pcm = pcm_open(rm->getVirtualSndCard(), pcmDevIds.at(0), PCM_OUT, &config);随后 pcm_start。数据写入在 SessionAlsaPcm::write,最终调 pcm_write:
status = pcm_write(pcm, data, sizeWritten);关闭在 close,pcm_close。SessionAlsaPcm 的数据通路就是 tinyalsa 的 pcm_open / pcm_start / pcm_write / pcm_stop / pcm_close,作用在 ResourceManager 扫到的虚拟声卡上。
SessionAgm 走的是另一条路。它只被 PAL_STREAM_NON_TUNNEL 选中,而且在整个 AIDL HAL 目录里 grep 不到 PAL_STREAM_NON_TUNNEL 的产生点,这个 build 的 AIDL HAL 不会触发它。它的 open 用的是 AGM 会话接口而不是 tinyalsa:
status = agm_session_open(sessionId, AGM_SESSION_NON_TUNNEL, &agmSessHandle);status = agm_session_register_cb(sessionId, eventCallback, AGM_EVENT_DATA_PATH, ...);status = agm_session_set_non_tunnel_mode_config(agmSessHandle, ...);数据走 agm_session_write_with_metadata / agm_session_read_with_metadata。两条路的差异:SessionAlsaPcm 把数据写进 ALSA PCM 设备,图在 ALSA 后端那一侧;SessionAgm 把数据交给 AGM 会话,图由 AGM 直接管理。两者名字里都带 non-tunnel,别混。
五、Device:设备对象究竟负责什么
Device 的工厂是 getInstance(device/src/Device.cpp),按 pal_device_id 分派到具体子类:PAL_DEVICE_OUT_SPEAKER 到 Speaker,PAL_DEVICE_OUT_WIRED_HEADSET 到 Headphone,PAL_DEVICE_OUT_BLUETOOTH_A2DP 到 BtA2dp,等等。
Device 不碰 PCM 数据。它的职责在 open/start/close/stop 里看。open 的一段:
rm->getBackendName(deviceAttr.id, backEndName);SessionAlsaUtils::setDeviceMediaConfig(rm, backEndName, &deviceAttr);rm->getAudioRoute(&audioRoute);rm->getSndDeviceName(deviceAttr.id, mSndDeviceName);enableDevice(audioRoute, mSndDeviceName);enableDevice 在 PalAudioRoute.h 里是个内联函数,就一行:
inlinevoidenableDevice(struct audio_route *ar, char *device_name){audio_route_apply_and_update_path(ar, device_name);}audio_route_apply_and_update_path 是 tinyalsa audio_route 库的接口,把一条命名路由(比如 "speaker")在 mixer 上使能,也就是打开 codec 侧到功放、扬声器的通路。close 对称地 disableDevice。start 里再设一次设备媒体配置,可能带 custom payload。
Device 管的是「端点路由」:把一个 PAL 逻辑设备映射成内核里一条命名 sound device 路由,在合适的时机 enable/disable。这和 Session 的 FE/BE 连接是两件事:Session 把 PCM 前端接到 DSP 后端,Device 把后端里的具体声学端点(扬声器、耳机)使能。
这四个层次要分清:
Device 类的名字容易让人以为它直接代表硬件,它只是把这些内核资源的使能动作封装成一个对象,开关动作都落在 mixer 控制和 audio_route 上。
六、把一条真实播放链路串起来
把前几节合到一起,一条 BUS 播放流的完整链是:

StreamOutPrimary::open 填好 streamAttributes_,调 pal_stream_open。 pal_stream_open 拿 ResourceManager 单例,调 Stream::create。 Stream::create 判定 PLAYBACK_BUS,new StreamPCM。 StreamPCM 构造里 makeSession 建 SessionAlsaPcm,getInstance 建 Device。 StreamPCM::open:session->open(分前端、连 FE/BE),再 device->open(使能路由)。 StreamPCM::start:device->start(设设备媒体配置),session->prepare,session->start(pcm_open 加 pcm_start)。 运行期写数据:StreamPCM::write 到 SessionAlsaPcm::write 到 pcm_write,采样数据进内核前端,经后端 AIF 进 ADSP。
七、PAL 与 AudioReach DSP 的关系

软件对象组织:Stream/Session/Device 的分层和委托,全部在 PAL 源码里,直接可读。 ALSA PCM 数据:SessionAlsaPcm 的 pcm_open/pcm_write 走虚拟声卡前端,进内核 ALSA。 mixer 配置:FE/BE 连接(FE_CONNECT)、元数据(FE_METADATA)、TKV/CKV(setParamTag/setCalibration)、设备路由(audio_route_apply_and_update_path)都是 mixer 控制。 DSP 图:前端通过 FE_CONNECT 连到某个后端名,那个后端名对应通往 ADSP 的 AIF。图在这一侧的实例化由 AGM/Graphite/GPR 完成,PAL 只负责发配置和喂数据。















































































